I'm trying to make an animation of a red oval that will move to the right of the screen. But it just draws the oval. I don't know what I'm doing wrong and I literally can't find anything about how to do this. Any help would be awesome, thanks.
import java.awt.*;
public class mainClass
{
public mainClass()
{
Frame f = new Frame("Canvas Example");
f.add(new MyCanvas());
f.setLayout(null);
f.setSize(400, 400);
f.setVisible(true);
}
public static void main(String args[])
{
new mainClass();
}
}
class MyCanvas extends Canvas
{
int x = 75;
public MyCanvas() {
setBackground (Color.BLACK);
setSize(400, 400);
}
public void paint(Graphics g)
{
g.setColor(Color.red);
g.fillOval(x, 75, 150, 75);
}
public void update(Graphics g)
{
x++;
}
}
Theory
Animation is hard, I mean, really good animation is hard. There is a lot of theory which goes into creating good animation, things like easement, anticipation, squish ... I could go on, but I'm boring myself.
The point is, simply incrementing a value (AKA linear progression) is a poor approach to animation. If the system is slow, busy or for some other reason isn't keeping up, the animation will suffer because of it (stuttering, pauses, etc).
A "better" solution is to use a time based progression. That is, you specify the amount of time it will take to move from the current state to it's new state and then continuously loop and update the state until you run out of time.
The "main loop"
If you do any research into game development, they always talk about this thing called the "main loop".
The "main loop" is responsible for updating the game state and scheduling paint passes.
In terms to your question, you need a "main loop" which can update the position of the oval until it reaches it's target position.
Because most GUI frameworks are already running within their own thread context, you need to setup your "main loop" in another thread
AWT
Some theory
AWT is the original GUI framework, so it's "old". While Swing does sit on top of it, you'll find more people have experience with Swing then they do AWT.
One of the important things to keep in mind is, Canvas is not double buffered, so, if you're updating the component fast enough, it will flash.
To overcome this, you need to implement some kind of double buffering workflow.
Runnable example
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.time.Duration;
import java.time.Instant;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
Frame frame = new Frame();
frame.add(new TestCanvas());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class Ticker implements Runnable {
public interface Callbck {
public void didTick(Ticker ticker);
}
private boolean isRunning = false;
private Thread thread;
private Callbck callback;
public void setCallback(Callbck tick) {
this.callback = tick;
}
public void start() {
if (isRunning) {
return;
}
isRunning = true;
thread = new Thread(this);
thread.setDaemon(false);
thread.start();
}
public void stop() {
if (!isRunning) {
return;
}
isRunning = false;
thread.interrupt();
thread = null;
}
#Override
public void run() {
while (isRunning) {
try {
Thread.sleep(5);
if (callback != null) {
callback.didTick(this);
}
} catch (InterruptedException ex) {
isRunning = false;
}
}
}
}
public class TestCanvas extends Canvas {
private BufferedImage buffer;
int posX;
private Ticker ticker;
private Instant startedAt;
private Duration duration = Duration.ofSeconds(5);
public TestCanvas() {
ticker = new Ticker();
ticker.setCallback(new Ticker.Callbck() {
#Override
public void didTick(Ticker ticker) {
if (startedAt == null) {
startedAt = Instant.now();
}
Duration runtime = Duration.between(startedAt, Instant.now());
double progress = runtime.toMillis() / (double)duration.toMillis();
if (progress >= 1.0) {
stopAnimation();
}
posX = (int)(getWidth() * progress);
repaint();
}
});
}
protected void startAnimtion() {
ticker.start();
}
protected void stopAnimation() {
ticker.stop();
}
#Override
public void setBounds(int x, int y, int width, int height) {
buffer = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
super.setBounds(x, y, width, height);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
#Override
public void addNotify() {
super.addNotify();
startAnimtion();
}
#Override
public void removeNotify() {
super.removeNotify();
buffer = null;
}
#Override
public void paint(Graphics g) {
super.paint(g);
if (buffer == null) {
return;
}
Graphics2D g2d = buffer.createGraphics();
g2d.setColor(getBackground());
g2d.fillRect(0, 0, getWidth(), getHeight());
g2d.setColor(Color.RED);
int midY = getHeight() / 2;
g2d.fillOval(posX, midY - 5, 10, 10);
g2d.dispose();
g.drawImage(buffer, 0, 0, this);
}
}
}
What is Canvas good for ...?
In most cases, you should avoid using Canvas, for many of the reasons mentioned above, but one of the reasons you might consider using Canvas is if you want to take full control over the painting process. You might do this if you want to create a complex game which and you want to get the best possible performance out of the rendering pipeline.
See BufferStrategy and BufferCapabilities and the JavaDocs for more detail
A Swing based implementation
Hopefully I've convinced you that a Swing implementation might be a better solution, which in that case you should make use of a Swing Timer instead of Thread, as Swing is not thread safe
See Concurrency in Swing and How to Use Swing Timers
for more details
Runnable example
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.time.Duration;
import java.time.Instant;
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() {
JFrame frame = new JFrame();
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class Ticker {
public interface Callbck {
public void didTick(Ticker ticker);
}
private Timer timer;
private Callbck callback;
public void setCallback(Callbck tick) {
this.callback = tick;
}
public void start() {
if (timer != null) {
return;
}
timer = new Timer(5, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if (callback == null) {
return;
}
callback.didTick(Ticker.this);
}
});
timer.start();
}
public void stop() {
if (timer == null) {
return;
}
timer.stop();
timer = null;
}
}
public class TestPane extends JPanel {
int posX;
private Ticker ticker;
private Instant startedAt;
private Duration duration = Duration.ofSeconds(5);
public TestPane() {
ticker = new Ticker();
ticker.setCallback(new Ticker.Callbck() {
#Override
public void didTick(Ticker ticker) {
if (startedAt == null) {
startedAt = Instant.now();
}
Duration runtime = Duration.between(startedAt, Instant.now());
double progress = runtime.toMillis() / (double) duration.toMillis();
if (progress >= 1.0) {
stopAnimation();
}
posX = (int) (getWidth() * progress);
repaint();
}
});
}
protected void startAnimtion() {
ticker.start();
}
protected void stopAnimation() {
ticker.stop();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
#Override
public void addNotify() {
super.addNotify();
startAnimtion();
}
#Override
public void removeNotify() {
super.removeNotify();
stopAnimation();
}
#Override
public void paint(Graphics g) {
super.paint(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(Color.RED);
int midY = getHeight() / 2;
g2d.fillOval(posX, midY - 5, 10, 10);
g2d.dispose();
}
}
}
The reason this doesn't animate is that nothing triggers the component to update and repaint itself. There are a few things that need to be considered:
Something needs to call the update method. Ordinarily, this is triggered by a call to repaint() on the component, but nothing in this code calls that method.
It's important for an overridden update method to call super.update(g) to ensure the default behavior is invoked (clearing the canvas and painting it again).
Animation has a time component: the oval should move over some period of time. This needs to be incorporated into the logic. AWT has no built-in mechanism for timed behavior.
If you're able to use classes from Swing, the javax.swing.Timer class is very useful for animation. It executes your callback on the AWT thread, and therefore means that you don't have to take special measures to ensure thread safety.
If you can't use Swing, it can use java.util.Timer or a custom thread, but will need to manage thread synchronization directly.
You'll probably also want the animation to stop once the oval reaches the edge of the canvas.
Here's an example using javax.swing.Timer (assuming Java 8 or later). Note that all of the animation logic is in the ActionListener attached to the Timer, so the overridden update method has been removed:
import javax.swing.*;
import java.awt.*;
public class MainClass {
public static final int CANVAS_SIZE = 400;
public MainClass() {
Frame f = new Frame("Canvas Example");
f.add(new MyCanvas(CANVAS_SIZE));
f.setLayout(null);
f.setSize(CANVAS_SIZE, CANVAS_SIZE);
f.setVisible(true);
}
public static void main(String[] args) {
new MainClass();
}
}
class MyCanvas extends Canvas {
public static final int INITIAL_POSITION = 75;
public static final int HEIGHT = 75;
public static final int WIDTH = 150;
private static final int TIMER_DELAY_MILLIS = 1000 / 30; // 30 FPS
private int x = INITIAL_POSITION;
private final Timer timer;
public MyCanvas(int canvasSize) {
setBackground(Color.BLACK);
setSize(canvasSize, canvasSize);
timer = new Timer(TIMER_DELAY_MILLIS, (event) -> {
// ensure the oval stays on the canvas
if (x + WIDTH < getWidth()) {
x++;
repaint();
} else {
stopAnimation();
}
});
timer.start();
}
public void paint(Graphics g) {
g.setColor(Color.red);
g.fillOval(x, INITIAL_POSITION, WIDTH, HEIGHT);
}
private void stopAnimation() {
timer.stop();
}
}
This code has a few additional incidental changes.
Updated the name of mainClass to MainClass (leading capital "M") to comply with standard Java naming conventions.
Changed String args[] to String[] args for the same reason.
Extracted numeric constants to named static final fields.
Made the canvas size a constructor parameter, controlled by the caller.
Made x private.
Minor formatting changes to ensure a consistent style.
One option that doesn't use javax.swing.Timer (with unchanged code omitted):
private final AtomicInteger x = new AtomicInteger(INITIAL_POSITION);
public MyCanvas(int canvasSize) {
setBackground(Color.BLACK);
setSize(canvasSize, canvasSize);
new Thread(() -> {
try {
// ensure the oval stays on the canvas
while (x.incrementAndGet() + WIDTH < getWidth()) {
Thread.sleep(TIMER_DELAY_MILLIS);
repaint();
}
} catch (InterruptedException e) {
// Just let the thread exit
Thread.currentThread().interrupt();
}
}).start();
}
Related
I am learning Java currently and have been given the assignmnet of finidhing off a program to create the game Conways's life (we started with some code provided to us and must add features etc to this).
I am currently stuck on a menu option for the game. I want it to start off at the menu screen, wherein buttons appear at the top for "Start", "Random", "Load", Save". I have written code so that the program displays these buttons, through a fillRect option in my paint method.
My question is, how do I use the mousePressed method to recognise the cells selected so that I can get an action to occur when they are selected. I been looking at this for a while but can't seem to get this working.
Any suggestion would be a massive help. I have shared my code below. It's a work in progress but I would really like to get this working before continuing on with the other functionality.
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.awt.image.*;
public class ConwaysLife extends JFrame implements Runnable, MouseListener {
// member data
private BufferStrategy strategy;
private Graphics offscreenBuffer;
private boolean gameState[][] = new boolean[40][40];
private boolean isGameInProgress = false;
// constructor
public ConwaysLife () {
//Display the window, centred on the screen
Dimension screensize = java.awt.Toolkit.getDefaultToolkit().getScreenSize();
int x = screensize.width/2 - 400;
int y = screensize.height/2 - 400;
setBounds(x, y, 800, 800);
setVisible(true);
this.setTitle("Conway's game of life");
// initialise double-buffering
createBufferStrategy(2);
strategy = getBufferStrategy();
offscreenBuffer = strategy.getDrawGraphics();
// register the Jframe itself to receive mouse events
addMouseListener(this);
// initialise the game state
for (x=0;x<40;x++) {
for (y=0;y<40;y++) {
gameState[x][y]=false;
}
}
// create and start our animation thread
Thread t = new Thread(this);
t.start();
}
// thread's entry point
public void run() {
while ( 1==1 ) {
// 1: sleep for 1/5 sec
try {
Thread.sleep(200);
} catch (InterruptedException e) { }
// 2: animate game objects [nothing yet!]
/*if (isGameInProgress == false) {
this.repaint();
}*/
// 3: force an application repaint
this.repaint();
}
}
// mouse events which must be implemented for MouseListener
public void mousePressed(MouseEvent e) {
while (!isGameInProgress) {
int x = e.getX()/20;
int y = e.getY()/20;
if(x >= 10 && x <= 80 && y >= 40 && y <= 65) {
isGameInProgress = !isGameInProgress;
this.repaint();
}
}
// determine which cell of the gameState array was clicked on
int x = e.getX()/20;
int y = e.getY()/20;
// toggle the state of the cell
gameState[x][y] = !gameState[x][y];
// request an extra repaint, to get immediate visual feedback
this.repaint();
}
public void mouseReleased(MouseEvent e) { }
public void mouseEntered(MouseEvent e) { }
public void mouseExited(MouseEvent e) { }
public void mouseClicked(MouseEvent e) { }
//
// application's paint method
public void paint(Graphics g) {
Font font = new Font("Veranda", Font.BOLD, 20);
g = offscreenBuffer; // draw to off screen buffer
// clear the canvas with a big black rectangle
g.setColor(Color.BLACK);
g.fillRect(0, 0, 800, 800);
/*look to add a while game in progress loop here!!!*/
// draw menu options
if(!isGameInProgress) {
g.setColor(Color.green);
g.fillRect(10, 40, 70, 25);
g.fillRect(100, 40, 100, 25);
g.fillRect(300, 40, 170, 25);
g.setColor(Color.BLACK);
g.setFont(font);
g.drawString("Start", 15, 60);
g.drawString("Random", 105, 60);
g.drawString("Load", 305, 60);
g.drawString("Save", 395, 60);
}
// redraw all game objects
g.setColor(Color.WHITE);
for (int x=0;x<40;x++) {
for (int y=0;y<40;y++) {
if (gameState[x][y]) {
g.fillRect(x*20, y*20, 20, 20);
}
}
}
// flip the buffers
strategy.show();
}
// application entry point
public static void main(String[] args) {
ConwaysLife w = new ConwaysLife();
}
}
You're not going to like the answer, but it's the "correct" way to approach the problem.
What you need to understand is, Swing/AWT is using a "passive" rendering workflow and BufferStrategy is using a "active" rendering workflow, these are incompatible with each other.
As a general rule, you should not be overriding paint of top level containers like JFrame, this is going to end in no end of issues. Instead, you should be starting with something like JPanel and overriding it's paintComponent method instead.
Having said that, there's a "simpler" solution available to you. CardLayout. This will allow you to seperate the workflows of the menu from the game and resolve the issue between Swing/AWT and BufferStrategy
For example...
import java.awt.Canvas;
import java.awt.CardLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferStrategy;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
public final class Main {
public static void main(String[] args) {
new Main();
}
public Main() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new JFrame();
frame.add(new MainPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class MainPane extends JPanel {
enum View {
MENU, GAME;
}
private CardLayout cardLayout = new CardLayout();
private GamePane gamePane;
public MainPane() {
setLayout(cardLayout);
gamePane = new GamePane();
add(new MenuPane(new MenuPane.Observer() {
#Override
public void startNewGame() {
showGame();
}
#Override
public void randomGame() {
}
#Override
public void loadGame() {
}
#Override
public void saveGame() {
}
}), View.MENU);
add(gamePane, View.GAME);
}
protected void add(Component compent, View view) {
add(compent, view.name());
}
protected void showGame() {
show(View.GAME);
gamePane.start();
}
protected void showMenu() {
gamePane.stop();
show(View.MENU);
}
protected void show(View view) {
cardLayout.show(this, view.name());
}
}
public class MenuPane extends JPanel {
public interface Observer {
public void startNewGame();
public void randomGame();
public void loadGame();
public void saveGame();
}
private Observer observer;
public MenuPane(Observer observer) {
this.observer = observer;
JButton startButton = new JButton("Start");
JButton randomButton = new JButton("Random");
JButton loadButton = new JButton("Load");
JButton saveButton = new JButton("Save");
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.ipadx = 10;
gbc.ipady = 10;
gbc.insets = new Insets(8, 8, 8, 8);
gbc.weightx = GridBagConstraints.REMAINDER;
add(startButton, gbc);
add(randomButton, gbc);
add(loadButton, gbc);
add(saveButton, gbc);
startButton.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
observer.startNewGame();
}
});
randomButton.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
observer.randomGame();
}
});
loadButton.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
observer.loadGame();
}
});
saveButton.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
observer.saveGame();
}
});
}
}
public class GamePane extends Canvas {
private Thread thread;
private volatile boolean isRunning = false;
public GamePane() {
setBackground(Color.BLACK);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(800, 800);
}
protected void start() {
if (isRunning) {
return;
}
createBufferStrategy(3);
isRunning = true;
thread = new Thread(new Runnable() {
#Override
public void run() {
mainLoop();
}
});
thread.start();
}
protected void stop() {
if (!isRunning || thread == null) {
return;
}
isRunning = false;
try {
thread.join();
} catch (InterruptedException ex) {
}
thread = null;
}
protected void mainLoop() {
try {
while (isRunning) {
render();
Thread.sleep(16);
}
} catch (InterruptedException ex) {
}
}
protected void render() {
BufferStrategy strategy = getBufferStrategy();
if (strategy == null) {
return;
}
// Render single frame
do {
// The following loop ensures that the contents of the drawing buffer
// are consistent in case the underlying surface was recreated
do {
// Get a new graphics context every time through the loop
// to make sure the strategy is validated
Graphics graphics = strategy.getDrawGraphics();
FontMetrics fm = graphics.getFontMetrics();
String text = "All your game are belong to us";
int x = (getWidth() - fm.stringWidth(text)) / 2;
int y = (getHeight() - fm.getHeight()) / 2;
graphics.setColor(Color.WHITE);
graphics.drawString(text, x, y + fm.getAscent());
// Render to graphics
// ...
// Dispose the graphics
graphics.dispose();
// Repeat the rendering if the drawing buffer contents
// were restored
} while (strategy.contentsRestored());
// Display the buffer
strategy.show();
// Repeat the rendering if the drawing buffer was lost
} while (strategy.contentsLost());
}
}
}
I would strongly recommend that you take the time to read through:
Creating a GUI With Swing
A Visual Guide to Layout Managers
Performing Custom Painting
Painting in AWT and Swing
BufferStrategy and BufferCapabilities
JavaDocs for BufferStrategy which demonstrate how the API should be used.
A "fully" BufferStrategy based approach...
Now, if you can't use Swing, "for reasons", you can still achieve a simular concept using "delegation".
Basically this means "delegating" responsibility for performing some workflow to another. In this case, we want to delegate the rendering and the handling of the mouse events.
This allows you to have a dedicated workflow for the menu and a dedicated workflow for the game, without having to try and mix a lot of state.
Why do I keep on insisting on separating these two workflows? Simply, because it makes it MUCH easier to manage and reason about, but also because it supports the Single Responsibility Principle.
The follow example makes use of Renderable interface to define the core functionality that end "render" delegate will need to implement, in this case, it's pretty simple, we want to tell the renderer to "render" it's content on each paint pass and we want to (optionally) delegate mouse clicked events (this could be done via a second interface or even just the MouseListener interface directly, but I've made it a requirement of the Renderable interface for demonstration purposes.
The "basic" solution to your actual question is found through the use of Rectangle#contains(Point).
This basically inspects each "button" Rectangle to determine if the supplied MouseEvent occurs within it's bounds, if it does, we take action.
It is, however, a little more complicated then that, as we need to have the Rectangles built ahead of time, not difficult, but it's state which is actually reliant on the parent, as we need to know the area in which the renderer is been displayed, run the example, you'll see what I mean 😉
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferStrategy;
import javax.swing.JFrame;
public final class Main {
public static void main(String[] args) {
new Main();
}
public Main() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
MainPane mainPane = new MainPane();
JFrame frame = new JFrame();
frame.add(mainPane);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
mainPane.start();
}
});
}
public interface Renderable {
public void render(Graphics2D g2d, Dimension size);
// We could just extend from MouseListener
// but I don't need all those event handlers
public void mouseClicked(MouseEvent e);
}
public class MainPane extends Canvas {
private Thread thread;
private volatile boolean isRunning = false;
private Renderable currentRenderer;
private MenuRenderer menuRenderer;
private GameRenderer gameRenderer;
public MainPane() {
setBackground(Color.BLACK);
gameRenderer = new GameRenderer();
menuRenderer = new MenuRenderer(new MenuRenderer.Observer() {
#Override
public void startNewGame() {
showGame();
}
#Override
public void randomGame() {
}
#Override
public void loadGame() {
}
#Override
public void saveGame() {
}
});
showMenu();
addMouseListener(new MouseAdapter() {
#Override
public void mouseClicked(MouseEvent e) {
if (currentRenderer == null) {
return;
}
currentRenderer.mouseClicked(e);
}
});
}
protected void showMenu() {
// This may need to tell the game renderer to stop
// or pause
currentRenderer = menuRenderer;
}
protected void showGame() {
currentRenderer = gameRenderer;
}
#Override
public Dimension getPreferredSize() {
return new Dimension(800, 800);
}
protected void start() {
if (isRunning) {
return;
}
createBufferStrategy(3);
isRunning = true;
thread = new Thread(new Runnable() {
#Override
public void run() {
mainLoop();
}
});
thread.start();
}
protected void stop() {
if (!isRunning || thread == null) {
return;
}
isRunning = false;
try {
thread.join();
} catch (InterruptedException ex) {
}
thread = null;
}
protected void mainLoop() {
try {
while (isRunning) {
render();
Thread.sleep(16);
}
} catch (InterruptedException ex) {
}
}
protected void render() {
BufferStrategy strategy = getBufferStrategy();
if (strategy == null && currentRenderer != null) {
return;
}
// Render single frame
do {
// The following loop ensures that the contents of the drawing buffer
// are consistent in case the underlying surface was recreated
do {
// Get a new graphics context every time through the loop
// to make sure the strategy is validated
Graphics2D g2d = (Graphics2D) strategy.getDrawGraphics();
g2d.setBackground(Color.BLACK);
g2d.fillRect(0, 0, getWidth(), getHeight());
RenderingHints hints = new RenderingHints(
RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON
);
g2d.setRenderingHints(hints);
// Render to graphics
currentRenderer.render(g2d, getSize());
// Dispose the graphics
g2d.dispose();
// Repeat the rendering if the drawing buffer contents
// were restored
} while (strategy.contentsRestored());
// Display the buffer
strategy.show();
// Repeat the rendering if the drawing buffer was lost
} while (strategy.contentsLost());
}
}
public class GameRenderer implements Renderable {
#Override
public void render(Graphics2D g2d, Dimension size) {
FontMetrics fm = g2d.getFontMetrics();
String text = "All your game are belong to us";
int x = (size.width - fm.stringWidth(text)) / 2;
int y = (size.height - fm.getHeight()) / 2;
g2d.setColor(Color.WHITE);
g2d.drawString(text, x, y + fm.getAscent());
}
#Override
public void mouseClicked(MouseEvent e) {
}
}
public class MenuRenderer implements Renderable {
public interface Observer {
public void startNewGame();
public void randomGame();
public void loadGame();
public void saveGame();
}
private Observer observer;
private String[] menuOptions = new String[]{
"New Game",
"Random",
"Load Game",
"Save Game"
};
private Rectangle[] menuBounds;
private int internalPadding = 20;
private int horizontalGap = 16;
public MenuRenderer(Observer observer) {
this.observer = observer;
}
#Override
public void render(Graphics2D g2d, Dimension size) {
if (menuBounds == null) {
createMenus(g2d, size);
}
renderMenus(g2d);
}
protected void createMenus(Graphics2D g2d, Dimension size) {
FontMetrics fm = g2d.getFontMetrics();
int totalHeight = (((fm.getHeight() + internalPadding) + horizontalGap) * menuOptions.length) - horizontalGap;
int buttonHeight = fm.getHeight() + internalPadding;
menuBounds = new Rectangle[menuOptions.length];
int buttonWidth = 0;
for (int index = 0; index < menuOptions.length; index++) {
int width = fm.stringWidth(menuOptions[index]) + internalPadding;
buttonWidth = Math.max(width, buttonWidth);
}
int yPos = (size.height - totalHeight) / 2;
for (int index = 0; index < menuOptions.length; index++) {
int xPos = (size.width - buttonWidth) / 2;
Rectangle menuRectangle = new Rectangle(xPos, yPos, buttonWidth, buttonHeight);
menuBounds[index] = menuRectangle;
yPos += buttonHeight + (horizontalGap / 2);
}
}
protected void renderMenus(Graphics2D g2d) {
for (int index = 0; index < menuOptions.length; index++) {
String text = menuOptions[index];
Rectangle bounds = menuBounds[index];
renderMenu(g2d, text, bounds);
}
}
protected void renderMenu(Graphics2D g2d, String text, Rectangle bounds) {
FontMetrics fm = g2d.getFontMetrics();
int textWidth = fm.stringWidth(text);
int textXPos = (bounds.x + (internalPadding / 2)) + ((bounds.width - internalPadding - textWidth) / 2);
int textYPos = bounds.y + (internalPadding / 2);
RoundRectangle2D buttonBackground = new RoundRectangle2D.Double(bounds.x, bounds.y, bounds.width, bounds.height, 20, 20);
g2d.setColor(Color.BLUE.darker());
g2d.fill(buttonBackground);
g2d.setColor(Color.WHITE);
g2d.drawString(text, textXPos, textYPos + fm.getAscent());
}
#Override
public void mouseClicked(MouseEvent e) {
if (menuBounds == null) {
return;
}
for (int index = 0; index < menuOptions.length; index++) {
if (menuBounds[index].contains(e.getPoint())) {
switch (index) {
case 0:
observer.startNewGame();
break;
case 2:
observer.randomGame();
break;
case 3:
observer.loadGame();
break;
case 4:
observer.saveGame();
break;
}
}
}
}
}
}
This is an UI that makes a ball go down in a diagonal way, but the ball stays static; it seems something is not working adecuatedly with the threads. Could you please, tell me how to make the ball move?
Please download a ball and change the directory so the program can find where your ball is allocated. It's not necessary to download the soccer pitch but if you want, it's OK. Finally, I have to thank you for spending time in search of this malfunctioning.
import java.awt.BorderLayout;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JButton;
import java.awt.event.ActionListener;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import javax.imageio.ImageIO;
import java.io.File;
class Animation extends JFrame implements ActionListener { //Frame and listener
Rectangle2D dimensions = new Rectangle2D.Double(0,0,850,595); //Not implemented limits
JButton animate, stop;
Runnable runnable;
Thread move;
public Animation() {
setLayout(new BorderLayout()); //BorderLayout disposition
setTitle("Pelota en acción");
animate = new JButton("Animate it!"); //Button to create balls
animate.setBounds(0,0,120,30);
animate.addActionListener(new ActionListener(){
#Override
public void actionPerformed(ActionEvent e) {
Image ball = null;
new Layout().createEllipse(ball);
runnable = new Layout();
move = new Thread(runnable);
move.start();
}
});
stop = new JButton("Freeze"); //Button to interrupt thread (not implemented)
stop.setBounds(0,0,120,30);
stop.addActionListener(new ActionListener(){
#Override
public void actionPerformed(ActionEvent e) {
move.interrupt();
Layout.running = false;
}
});
JPanel subPanel = new JPanel(); //Layout with its buttons situated to the south
subPanel.add(animate);
subPanel.add(stop);
add(subPanel,BorderLayout.SOUTH);
add(new Layout());
}
public static void main(String[] args) {
Animation ventana = new Animation();
ventana.setSize(850,625);
ventana.setLocationRelativeTo(null);
ventana.setVisible(true);
ventana.setDefaultCloseOperation(EXIT_ON_CLOSE);
}
#Override
public void actionPerformed(ActionEvent e) {} //Tag
} //Class close
class Layout extends JPanel implements Runnable { //Layout and thread
int X,Y; //Coordenadas
static boolean running = true; //"To interrupt the thread" momentaneously.
static ArrayList<Image> balls = new ArrayList<>(); //Balls collection
#Override
public void run () { //Just moves ball towards Narnia xd
while(running) {
X++; Y++;
System.out.println(X+" "+Y);
repaint();
updateUI();
try {
Thread.sleep(4);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D)g;
g2.addRenderingHints(new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON));
repaint();
updateUI();
try {
URL url = new URL("https://www.freejpg.com.ar/image-900/9c/9ca2/F100004898-textura_pasto_verde_linea_de_cal.jpg");
Image picture = ImageIO.read(url);
g.drawImage(picture,0,0,null);
} catch(IOException e){
System.out.println("URL image was not found");
}
finally {
try {
//----------------------------------------------------------------------------
Image picture = ImageIO.read(new File("C:\\Users\\Home\\Desktop\\Cancha.jpg")); //Pitch
//----------------------------------------------------------------------------
g.drawImage(picture, 0, 0, null);
} catch (IOException ex) {
System.out.println("Pitch image was not found");
}
}
for (Image ball : balls) { //I add balls to the Layout
g2.drawImage(ball,X,Y,100,100,null);
}
}
public void createEllipse (Image ball) { //Method that adds balls to the collection
try {
//-------------------------------------------------------------------- Ball
ball = ImageIO.read(new File("C:\\Users\\Home\\Desktop\\Pelota.png")); //Change this
//-------------------------------------------------------------------- Ball
} catch(IOException ex) {
System.out.println("Any balls were found");
}
balls.add(ball);
}
}
So to break your code down:
When the button is pressed, you execute the following code:
Image ball = null;
new Layout().createEllipse(ball);
runnable = new Layout();
move = new Thread(runnable);
move.start();
This will create a new layout. The run() method of this will increase the X and Y variables. They are declared here:
int X,Y; //Coordenadas
Those are instance variables, this means they belong to your newly created Layout.
Then you call repaint() on the new Layout, which will do nothing, because this new Layout has not been added to some window.
So, how do you fix this?
First, you have to keep the original Layout around:
class Animation extends JFrame { // no need to implement ActionListener
Rectangle2D dimensions = new Rectangle2D.Double(0,0,850,595); //Not implemented limits
JButton animate, stop;
Thread move;
Layout layout;
Then remember the Layout when you create it:
// before: add(new Layout());
layout = new Layout();
add(layout);
Then use the layout in your ActionListener:
layout.createEllipse(ball);
move = new Thread(layout);
move.start();
This might have some problems with concurrency (Swing is not thread-safe), so for good measure, you should call repaint() in the AWTEventThread:
// in run(), was repaint():
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
repaint();
}
});
Now, there are some cleanup tasks left:
Delete this code:
#Override
public void actionPerformed(ActionEvent e) {} //Tag
It's no longer needed, because you don't implement ActionListener.
Drop the static modifiers from some fields, and add volatile:
volatile int X,Y; //Coordenadas
volatile boolean running = true; //"To interrupt the thread" momentaneously.
ArrayList<Image> balls = new ArrayList<>(); //Balls collection
volatile is needed for variables that are accessed from more than one thread.
Also remove repaint() and resetUI() from the paint method. You don't need them.
For the pictures in paint: you should cache them. Store them in a field, so you don't have to load the picture every time.
When all this is done, your code is much cleaner, but there are still some warts that should be addressed. But at least you have something working.
Johannes has already spoken about many of the things which are wrong with your original example, so I won't go over many of them again.
This example makes use of a Swing Timer instead of a Thread as the main "animation" loop. It also focuses on demonstrating encapsulation and responsibility.
For example, the AnimtionPane is responsible for managing the balls, managing the animation loop and paint. It isn't, however, responsible for determining "how" the balls are updated or paint, it only provides the timing and functionality to make those things happen.
A couple of the glaring issues I can see are:
Trying to load resources from within the paintComponent method. This is a bad ideas, as it could slow you paint pass down, causing your UI to lag
Calling repaint and updateUI from within the paintComponent method. You should avoid causing any new updates to the UI from occurring during a paint process. This could cause your program to run wide and consume all the CPU cycles, not only making your app non-responsive, but also the whole system.
Some very quick points
Swing is not thread safe. You should never update the UI (or anything the UI relies on) from outside the context of the Event Dispatching Thread. This example uses a Swing Timer as it allows the delay to occur of the EDT (and not block the UI), but it's updates are triggered within the EDT, allowing us to safely update the UI from within
You create multiple instances of Layout, meaning that the one on the screen isn't the one which is been updated
Your "freeze" logic is broken. It will never "freeze" anything
Runnable example
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Ellipse2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import javax.swing.JButton;
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() {
JFrame frame = new JFrame();
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
private AnimationPane animationPane;
public TestPane() {
setLayout(new BorderLayout());
animationPane = new AnimationPane();
JButton actionButton = new JButton("Start");
actionButton.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent evt) {
if (animationPane.isAnimating()) {
animationPane.stop();
actionButton.setText("Start");
} else {
animationPane.start();
actionButton.setText("Stop");
}
}
});
add(animationPane);
add(actionButton, BorderLayout.SOUTH);
}
}
// This is just makes it seem more random ;)
private static Random RANDOM = new Random();
public class Ball {
private int x;
private int y;
private int xDelta;
private int yDelta;
private Color color;
private Shape shape;
public Ball(Color color) {
shape = new Ellipse2D.Double(0, 0, 10, 10);
this.color = color;
// Get some random motion
do {
xDelta = RANDOM.nextInt(6) + 2;
yDelta = RANDOM.nextInt(6) + 2;
} while (xDelta == yDelta);
}
public void update(Rectangle bounds) {
x += xDelta;
y += yDelta;
if (x + 10 > bounds.x + bounds.width) {
x = bounds.x + bounds.width - 10;
xDelta *= -1;
} else if (x < bounds.x) {
x = bounds.x;
xDelta *= -1;
}
if (y + 10 > bounds.y + bounds.height) {
y = bounds.y + bounds.height - 10;
yDelta *= -1;
} else if (y < bounds.y) {
y = bounds.y;
yDelta *= -1;
}
}
public void paint(Graphics2D g2d) {
// This makes it easier to restore the graphics context
// back to it's original state
Graphics2D copy = (Graphics2D) g2d.create();
copy.setColor(color);
copy.translate(x, y);
copy.fill(shape);
// Don't need the copy any more, get rid of it
copy.dispose();
}
}
public class AnimationPane extends JPanel {
// This does not need to be static
private List<Ball> balls = new ArrayList<>(); //Balls collection
private Timer timer;
private List<Color> colors;
public AnimationPane() {
colors = new ArrayList<>(8);
colors.add(Color.RED);
colors.add(Color.GREEN);
colors.add(Color.BLUE);
colors.add(Color.CYAN);
colors.add(Color.MAGENTA);
colors.add(Color.ORANGE);
colors.add(Color.PINK);
colors.add(Color.YELLOW);
timer = new Timer(40, new ActionListener() {
#Override
public void actionPerformed(ActionEvent evt) {
if (RANDOM.nextBoolean()) {
makeBall();
}
Rectangle bounds = new Rectangle(new Point(0, 0), getSize());
for (Ball ball : balls) {
ball.update(bounds);
}
repaint();
}
});
makeBall();
}
protected void makeBall() {
Collections.shuffle(colors);
balls.add(new Ball(colors.get(0)));
}
public boolean isAnimating() {
return timer.isRunning();
}
public void start() {
timer.start();
}
public void stop() {
timer.stop();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g.create();
g2.addRenderingHints(new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON));
// Bad ideas. Repaint will cause a new paint event to be posted, causing your
// UI to run away - consuming all your CPU cycles in a singulator forms
// and destorys the known universe
//repaint();
// This doesn't do what you think it does and there shouldn't be
// reason for you to call it
//updateUI();
// This is a bad idea as it could cause the paint cycles to slow down
// destorying the responsiveness of your app
// Besids, you should be passing this as the ImageObserver
// try {
// URL url = new URL("https://www.freejpg.com.ar/image-900/9c/9ca2/F100004898-textura_pasto_verde_linea_de_cal.jpg");
// Image picture = ImageIO.read(url);
// g.drawImage(picture, 0, 0, null);
// } catch (IOException e) {
// System.out.println("URL image was not found");
// } finally {
// try {
// //----------------------------------------------------------------------------
// Image picture = ImageIO.read(new File("C:\\Users\\Home\\Desktop\\Cancha.jpg")); //Pitch
// //----------------------------------------------------------------------------
// g.drawImage(picture, 0, 0, null);
// } catch (IOException ex) {
// System.out.println("Pitch image was not found");
// }
// }
// This is "bad" per say, but each ball should have it's own
// concept of location
// for (Image ball : balls) { //I add balls to the Layout
// g2.drawImage(ball, X, Y, 100, 100, null);
// }
for (Ball ball : balls) {
ball.paint(g2);
}
// I made a copy of the graphics context, as this is shared
// with all the other components been painted, changing the
// render hints could cause issues
g2.dispose();
}
}
}
I am currently using a JPanel to draw the elements from my game, however, there is occasional stuttering with the graphics, after some research I learnt about screen tearing and double buffering, which I believe is the solution to my problem, however, currently with my code I am finding it extremely difficult to implement without tearing out a lot of code.
I am wondering if anyone could possibly provide either a simpler way to fix my screen tearing on a small 2D tile based game, or how to do double buffering with my current code, thank you very much!
Frame.java
public class Frame extends JPanel implements KeyListener {
public static Game game;
public static boolean[] mouseDown = new boolean[4];
public static int width, height;
public static Font font = new Font("Arial", Font.BOLD, 16);
public static JFrame frame;
public Frame()
{
setFocusable(true);
requestFocus();
setOpaque(true);
addKeyListener((KeyListener) this);
addMouseListener(new MouseAdapter() {
#Override
public void mousePressed(MouseEvent e) {
}
#Override
public void mouseReleased(MouseEvent e) {
}
});
addMouseMotionListener(new MouseMotionListener() {
#Override
public void mouseMoved(MouseEvent e) {
}
#Override
public void mouseDragged(MouseEvent e) {
}
});
}
#Override
public void paint(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
g2d.setBackground(Color.BLACK);
g2d.setFont(font);
super.setBackground(Color.BLACK);
super.paintComponent(g2d);
//This is where I use g2d to draw onto the JPanel
}
public static void main(String[] args) {
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
frame = new JFrame("Game");
frame.add(new Frame());
frame.setSize(1000, 750);
frame.setExtendedState(frame.getExtendedState() | JFrame.MAXIMIZED_BOTH);
frame.setVisible(true);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}});
}
Game.java
public void gameStart() {
Thread gameThread = new Thread() {
// Override run() to provide the running behavior of this thread.
#Override
public void run() {
gameLoop();
}
};
// Start the thread. start() calls run(), which in turn calls gameLoop().
gameThread.start();
}
public void gameLoop() {
while (!isGameFinished) {
beginTime = System.nanoTime();
gameUpdate();
frame.repaint();
// Delay timer to provide the necessary delay to meet the target rate
timeTaken = System.nanoTime() - beginTime;
timeLeft = (UPDATE_PERIOD_NSEC - timeTaken) / 1000000L; // in milliseconds
if (timeLeft < 10) timeLeft = 10; // set a minimum
try { // Provides the necessary delay and also yields control so that other thread can do work.
Thread.sleep(timeLeft);
} catch (InterruptedException ex) { }
}
}
public void gameStart() {
Thread gameThread = new Thread() {
// Override run() to provide the running behavior of this thread.
#Override
public void run() {
gameLoop();
}
};
// Start the thread. start() calls run(), which in turn calls gameLoop().
gameThread.start();
}
public void gameLoop() {
while (!isGameFinished) {
beginTime = System.nanoTime();
gameUpdate();
frame.repaint();
timeTaken = System.nanoTime() - beginTime;
timeLeft = (UPDATE_PERIOD_NSEC - timeTaken) / 1000000L;
if (timeLeft < 10) timeLeft = 10; // set a minimum
try {
Thread.sleep(timeLeft);
} catch (InterruptedException ex) { }
}
}
As shown in the code, the Game class contains the Game loop, which will constantly repaint the Frame class. How can I tweak this code to allow for double buffering to remove screen tearing? Thanks!
Double buffering is the best way to fix it. Try calling Toolkit#sync (Toolkit.getDefaultToolkit().sync()) , which sometimes removes the issue. You won't be tearing down much code if you switch to double buffering, just move the code in paint to a separate method.
I'm developing a very simple version of R-Type as work for the university, but despite it works, the craft velocity is a lot of slow, so the movement is ugly and clumsy.
I use the method repaint for refresh the screen, there are others methods or ways best than it?
Video of Movement
Paint method at main Panel
#Override
public void paint(Graphics g) {
super.paint(g);
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2.drawImage(fondo, 0, 0,1200,600,this);
pj.paint(g2);
g2D=g2;
}
PJ's paint method
public void paint(Graphics2D g) {
g.drawImage(imagen,x,y,this);
}
PJ's move method
public void move (KeyEvent e) {
int dx = 0; int dy = 0;
int code = e.getKeyCode();
switch (code) {
case KeyEvent.VK_Q: dy-=1; break;
case KeyEvent.VK_A: dy+=1; break;
case KeyEvent.VK_P: dx+=1; break;
case KeyEvent.VK_O: dx-=1; break;
}
int x = (getX()<maxX&&getX()!=0) ? getX()+dx : getX();
int y = (getY()<maxY&&getY()!=0) ? getY()+dy : getY();
if (getY()>=maxY||getY()==0) {
if (dy==+1) y=y+1;
}
setPosicion(x, y);
}
The image fondo should already be scaled to 1200x600.
I am not sure, but is super.paint(g) needed? You might also use paintComponent.
The event handling (you seem to be moving by 1 pixel on key down), must be done correctly. I would have set the direction and speed (1px), and leave it to a swing timer to do the continuous moving.
Repainting best is done resilient/flexible: repaint(20L) (50 frames per second);
events like key-down maybe with EventQueue.invokeLater(new Runnable() { ... });.
Especially you might use repaint with the changed area.
You can find a great example of a similar program here. The example demonstrates creating a new thread and having that thread sleep every iteration through the main loop.
Here is another question about loading images for games in Java.
It looks like swing itself is pretty crummy for using images in games. You may want to consider using a more suitable library.
Below is simple example using a background as simple game loop. It updates the state of the game objects and calculates the required delay in order to maintain the required fps.
The game object (Ship) has the ability to accelerate/decelerate over a short period of time
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.geom.Path2D;
import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class AnimationTest {
public static void main(String[] args) {
new AnimationTest();
}
public AnimationTest() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
}
JFrame frame = new JFrame("Test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new GamePane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class GamePane extends JPanel {
private Ship ship;
public GamePane() {
ship = new Ship();
Thread thread = new Thread(new MainLoop(this));
thread.setDaemon(true);
thread.start();
InputMap im = getInputMap(WHEN_IN_FOCUSED_WINDOW);
ActionMap am = getActionMap();
// Key controls...
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, false), "upPressed");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, false), "downPressed");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, true), "upReleased");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, true), "downReleased");
am.put("upPressed", new AbstractAction() {
#Override
public void actionPerformed(ActionEvent e) {
// Change the direction...
ship.setDirection(-1);
// Accelerate by 1 per frame
ship.setVelocity(1);
}
});
am.put("downPressed", new AbstractAction() {
#Override
public void actionPerformed(ActionEvent e) {
// Change direction
ship.setDirection(1);
// Accelerate by 1 per frame
ship.setVelocity(1);
}
});
am.put("upReleased", new AbstractAction() {
#Override
public void actionPerformed(ActionEvent e) {
// Deccelerate by 1 per frame
ship.setVelocity(-1);
}
});
am.put("downReleased", new AbstractAction() {
#Override
public void actionPerformed(ActionEvent e) {
// Deccelerate by 1 per frame
ship.setVelocity(-1);
}
});
}
#Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
public void updateState() {
// Update the state of the game objects.
// This would typically be better done in
// some kind of model
ship.update(getWidth(), getHeight());
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
// Paint the game state...
Graphics2D g2d = (Graphics2D) g.create();
ship.paint(g2d);
g2d.dispose();
}
}
public class MainLoop implements Runnable {
private GamePane pane;
private int fps = 25;
public MainLoop(GamePane pane) {
this.pane = pane;
}
#Override
public void run() {
// Wait until the screen is ready
while (pane.getHeight() <= 0) {
try {
Thread.sleep(125);
} catch (InterruptedException ex) {
}
}
// Main loop
while (true) {
// Start time loop
long startTime = System.currentTimeMillis();
// Update the game state
pane.updateState();
// Calculate the amount of time it took to update
long elasped = System.currentTimeMillis() - startTime;
// Calculate the number of milliseconds we need to sleep
long sleep = Math.round((1000f / fps) - elasped);
pane.repaint();
if (sleep > 0) {
try {
Thread.sleep(sleep);
} catch (InterruptedException ex) {
}
}
}
}
}
public static class Ship {
public static int MAX_SPEED = 8;
private int direction = 0;
private int velocity = 0;
private int x;
private int y;
private int speed = 0;
private Path2D shape;
private boolean initState;
public Ship() {
shape = new Path2D.Float();
shape.moveTo(0, 0);
shape.lineTo(5, 5);
shape.lineTo(0, 10);
shape.lineTo(0, 0);
shape.closePath();
initState = true;
}
public void setDirection(int direction) {
this.direction = direction;
}
public void setVelocity(int velocity) {
this.velocity = velocity;
}
public void update(int width, int height) {
if (initState) {
y = (height - 10) / 2;
initState = false;
} else {
// Add the velocity to the speed
speed += velocity;
// Don't over accelerate
if (speed > MAX_SPEED) {
speed = MAX_SPEED;
} else if (speed < 0) {
speed = 0;
}
// Adjust out position if we're moving
if (speed > 0) {
y += (direction * speed);
}
// Bounds check...
if (y - 5 < 0) {
y = 5;
} else if (y + 5 > height) {
y = height - 5;
}
}
}
public void paint(Graphics2D g2d) {
int yPos = y - 5;
g2d.translate(10, yPos);
g2d.fill(shape);
g2d.translate(-10, -yPos);
}
}
}
I'm trying to draw two lines in a Canvas in Java, calling two methods separately, but when I draw the second line, the first one disapears (Java clears the screen). How can I avoid that? I want to see the two lines. I've seen paint tutorials (how to make a program like the Paint on Windows) where the user uses the mouse to draw lines and when one line is drawn, the other do not disappear. They just call the paint method and it does not clear the screen.
I'll be grateful if anyone can help me.
Thanks.
View Class
import java.awt.BorderLayout;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Graphics;
import javax.swing.JFrame;
public class CircuitTracePlotView extends JFrame {
private CircuitTracePlot circuitTracePlot;
public CircuitTracePlotView() {
this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
this.getContentPane().add(circuitTracePlot = new CircuitTracePlot(), BorderLayout.CENTER);
this.pack();
this.setSize(250,250);
this.setLocationRelativeTo(null);
this.setVisible(true);
circuitTracePlot.drawLine();
circuitTracePlot.drawOval();
}
}
class CircuitTracePlot extends Canvas {
private final static short LINE = 1;
private final static short OVAL = 2;
private int paintType;
private int x1;
private int y1;
private int x2;
private int y2;
public CircuitTracePlot() {
this.setSize(250,250);
this.setBackground(Color.WHITE);
}
private void setPaintType(int paintType) {
this.paintType = paintType;
}
private int getPaintType() {
return this.paintType;
}
public void drawLine() {
this.setPaintType(LINE);
this.paint(this.getGraphics());
}
public void drawOval() {
this.setPaintType(OVAL);
this.paint(this.getGraphics());
}
public void repaint() {
this.update(this.getGraphics());
}
public void update(Graphics g) {
this.paint(g);
}
public void paint(Graphics g) {
switch (paintType) {
case LINE:
this.getGraphics().drawLine(10, 10, 30, 30);
case OVAL:
this.getGraphics().drawLine(10, 20, 30, 30);
}
}
}
Main class
import javax.swing.SwingUtilities;
import view.CircuitTracePlotView;
public class Main {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
CircuitTracePlotView cr = new CircuitTracePlotView();
}
});
}
}
You almost never should call paint(...) directly. I can count the times that I've needed to do this on one hand.
Do not get a Graphics object by calling getGraphics() on a component as that will return a non-durable Graphics object. Instead either draw in a BufferedImage and display that in the paint method or draw in the paint method (if AWT).
Since this is a Swing GUI, don't use an AWT component to draw in. Use a JPanel and override the paintComponent(...) method, not the paint(...) method. Otherwise you lose all benefits of Swing graphics including automatic double buffering.
The super.paintComponent(g) method should be called in the paintComponent(Graphics g) override, often as the first method call inside of this method. This lets the component do its own housekeeping painting, including erasing drawings that need to be erased.
Read the tutorials on Swing graphics as most of this is all well explained there. For e.g., please have a look here:
Lesson: Performing Custom Painting
Painting in AWT and Swing
Edit
To have your images persist, I suggest that you draw to a BufferedImage and then display that Image in your JPanel's paintComponent(...) method.
Or another option is to create a Collection of Shape objects, perhaps an ArrayList<Shape> and fill it with the Shapes you'd like to draw, and then in the paintComponent(...) method cast the Graphics object to a Graphics2D object and iterate through the Shape collection drawing each shape with g2d.draw(shape) as you iterate.
Since Trash posted his code,...
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.*;
public class CircuitTracePlot2 extends JPanel {
private static final int PREF_W = 250;
private static final int PREF_H = PREF_W;
private int drawWidth = 160;
private int drawHeight = drawWidth;
private int drawX = 10;
private int drawY = 10;
private PaintType paintType = PaintType.LINE;
public CircuitTracePlot2() {
}
#Override
public Dimension getPreferredSize() {
return new Dimension(PREF_W, PREF_H);
}
public void setPaintType(PaintType paintType) {
this.paintType = paintType;
repaint();
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (paintType == null) {
return;
}
switch (paintType) {
case LINE:
g.drawLine(drawX, drawY, drawWidth, drawHeight);
break;
case OVAL:
g.drawOval(drawX, drawY, drawWidth, drawHeight);
break;
case SQUARE:
g.drawRect(drawX, drawY, drawWidth, drawHeight);
default:
break;
}
}
private static void createAndShowGui() {
final CircuitTracePlot2 circuitTracePlot = new CircuitTracePlot2();
JFrame frame = new JFrame("CircuitTracePlot2");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(circuitTracePlot);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
int timerDelay = 2 * 1000;
new Timer(timerDelay , new ActionListener() {
private int paintTypeIndex = 0;
#Override
public void actionPerformed(ActionEvent arg0) {
paintTypeIndex++;
paintTypeIndex %= PaintType.values().length;
circuitTracePlot.setPaintType(PaintType.values()[paintTypeIndex]);
}
}).start();
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGui();
}
});
}
}
enum PaintType {
LINE, OVAL, SQUARE;
}
Here's a variation on your program that implements much of #Hovercraft's helpful advice. Try commenting out the call to setPaintType() to see the effect.
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
/** #see http://stackoverflow.com/a/15854246/230513 */
public class Main {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
CircuitTracePlotView cr = new CircuitTracePlotView();
}
});
}
private static class CircuitTracePlotView extends JFrame {
private CircuitTracePlot plot = new CircuitTracePlot();
public CircuitTracePlotView() {
this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
plot.setPaintType(CircuitTracePlot.OVAL);
this.add(plot, BorderLayout.CENTER);
this.pack();
this.setLocationRelativeTo(null);
this.setVisible(true);
}
}
private static class CircuitTracePlot extends JPanel {
public final static short LINE = 1;
public final static short OVAL = 2;
private int paintType;
public CircuitTracePlot() {
this.setBackground(Color.WHITE);
}
public void setPaintType(int paintType) {
this.paintType = paintType;
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
switch (paintType) {
case LINE:
g.drawLine(10, 10, 30, 30);
case OVAL:
g.drawOval(10, 20, 30, 30);
default:
g.drawString("Huh?", 5, 16);
}
}
#Override
public Dimension getPreferredSize() {
return new Dimension(250, 250);
}
}
}