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;
}
}
}
}
}
}
Related
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();
}
For some reason, my KeyListener works just fine and fires off the Booleans to make down and up true and false and the y value changes according to those Booleans exactly how I want it to. My problem is that for some reason, the red rectangle appears to grow in size rather than move, and I'm pretty sure that it's because the previous frame is not cleared. I tried to use super.paintComponent(g); to clear the frame but this accomplishes nothing. Here's the code:
JFrame:
import java.awt.*;
import javax.swing.*;
public class H extends JFrame
{
public H()
{
super("Atlas Blade");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setResizable(false);
pack();
P p = new P();
Insets frameInsets = getInsets();
int frameWidth = p.getWidth() +
(frameInsets.left + frameInsets.right);
int frameHeight = p.getHeight() + (
frameInsets.top + frameInsets.bottom);
setPreferredSize(new Dimension(frameWidth, frameHeight));
setLayout(null);
add(p);
pack();
setVisible(true);
}
}
JPanel:
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseEvent;
import java.awt.image.*;
public class P extends JPanel implements KeyListener, Runnable
{
private long updateCount=0;
private long paintCount=0;
private int updatesPerSecond = 50;
private boolean aLeft,aRight,aDown,aUp=false;
private boolean up,down,left,right=false;
int x = 20;
int y=20;
Hb box = new Hb(x,y);
Rectangle rect = new Rectangle(0,300,300,50);
BufferedImage buffer;
public P()
{
super();
setSize(600,350);
//setSize(50,50);
buffer = new BufferedImage (600,350,BufferedImage.TYPE_4BYTE_ABGR);
addKeyListener(this);
Thread jim = new Thread(this);
jim.start();
}
public void run()
{
int waitToUpdate = 1000/updatesPerSecond;
long startTime = System.nanoTime();
while(true)
{
boolean shouldRepaint = false;
long currentTime = System.nanoTime();
long updatesNeeded = (((currentTime-startTime) / 1000000))/ waitToUpdate;
for(long x = updateCount; x< updatesNeeded; x++)
{
updateGame();
shouldRepaint=true;
updateCount++;
}
if(shouldRepaint)
{
paintCount++;
repaint();
}
try
{
Thread.sleep(5);
}
catch(Exception e)
{
e.printStackTrace();
}
}
}
#Override
public void paintComponent(Graphics g)
{
super.paintComponent(g);
Graphics bg = buffer.getGraphics();
bg.setColor(Color.BLACK);
bg.drawRect(0,300,300,50);
bg.setColor(Color.RED);
bg.fillRect(x,y,35,35);
g.drawImage(buffer,0,0,null);
}
public void updateGame()
{
box.updateHitbox(x,y);
if(down)
{
if(!box.center.intersects(rect))
{
y++;
//y=y+40;
}
}
else if(up)
{
if(!box.center.intersects(rect))
{
y--;
}
}
}
public void keyPressed(KeyEvent e)
{
int code = e.getKeyCode();
if(code==KeyEvent.VK_A)
{
left=true;
right=false;
aLeft=true;
aRight=false;
aDown=false;
aUp=false;
}
if(code==KeyEvent.VK_D)
{
left=false;
right=true;
aLeft=false;
aRight=true;
aDown=false;
aUp=false;
}
if(code==KeyEvent.VK_S)
{
System.out.println(y);
down=true;
up=false;
aLeft=false;
aRight=false;
aDown=true;
aUp=false;
}
if(code==KeyEvent.VK_W)
{
down=false;
up=true;
aLeft=false;
aRight=false;
aDown=false;
aUp=true;
}
repaint();
}
public void keyTyped(KeyEvent e)
{
}
public void keyReleased(KeyEvent e)
{
int code = e.getKeyCode();
if(code==e.VK_A)
{
left=false;
aLeft=false;
}
if(code==e.VK_D)
{
right=false;
aRight=false;
}
if(code==e.VK_S)
{
down=false;
aDown=false;
}
if(code==e.VK_W)
{
up=false;
aUp=false;
}
}
public void addNotify()
{
// call super so the method still does what it was built to do
super.addNotify();
// requests focus so that key inputs are sent to this screen
requestFocus();
}
}
And the Hb class:
import java.awt.Rectangle;
public class Hb
{
public Rectangle center,left,right,up,down;
public Hb(int x, int y)
{
center = new Rectangle(x,y,50,50);
left = new Rectangle(x-1,y+1,1,48);
right = new Rectangle(x+50,y+1,1,48);
up = new Rectangle(x+1,y-1,48,1);
down = new Rectangle(x+1,y+50,48,1);
}
public void updateHitbox(int x, int y)
{
center = new Rectangle(x,y,50,50);
left = new Rectangle(x-1,y+1,1,48);
right = new Rectangle(x+50,y+1,1,48);
up = new Rectangle(x+1,y-1,48,1);
down = new Rectangle(x+1,y+50,48,1);
}
}
Your problem is that you're doing all your drawing in the BufferedImage, and that doesn't allow erasure of "dirty" pixels. Instead, only draw in the BufferedImage that which should be a static and unchanging part of the image, usually the background. The foreground image that moves should be painted directly in paintComponent using the Graphcis object given to the method by the JVM.
public P() {
super();
setSize(600, 350); // not recommended
buffer = new BufferedImage(600, 350, BufferedImage.TYPE_4BYTE_ABGR);
Graphics bg = buffer.getGraphics();
bg.setColor(Color.BLACK);
bg.drawRect(0, 300, 300, 50);
bg.dispose();
// ....
}
and
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawImage(buffer, 0, 0, null);
g.setColor(Color.RED);
g.fillRect(x, y, 35, 35);
}
Vertical bars should be filling to the height of the applet. When the top is reached, a new bar should start filling next to the previous. Problem: When the new bar starts filling the previous paint() /bar is cleared
img how it is: http://bayimg.com/DAEoeaagm
img how it should be: http://bayimg.com/dAeOgAaGm
the code:
import java.awt.Color;
import java.awt.Graphics;
import javax.swing.JApplet;
public class fillingbar extends JApplet implements Runnable{
int shifting=0,filling=0;
public void init()
{
Thread t= new Thread(this);
t.start();
setSize(400,250);
}
public void paint(Graphics g)
{
super.paint(g);
g.setColor(Color.GREEN);
g.fillRect(shifting,getHeight()-filling,20,filling);
g.setColor(Color.BLACK);
g.drawRect(shifting, getHeight()-filling, 20, filling);
}
public void run()
{
while(true)
{
repaint();
try{
if(shifting<getWidth())
{
if(filling<getHeight())
filling+=10;
else {
shifting+=20;
filling=0;
}
}
Thread.sleep(50);
}catch(Exception E){
System.out.println("Exception caught");
}
}
}
}
You only draw one rectangle in your paint method, and so it makes sense that only one will show.
If you need to draw more, do so, using a for loop that loops through perhaps a Rectangle ArrayList<Rectangle>.
Another way is to make shifting local and do a bit of simple math inside paintComponent to see what to draw and where. For instance, draw your completed bars inside of a for loop, for (int i = 0; i < filling / getHeight(); i++) {, and your yet to be completed bar up to filling % getHeight().
You should not draw directly within a JApplet but rather in the paintComponent method of a JPanel.
A Swing Timer is easier to use than a thread (for me at least), and can be safer.
For example, this can be created by the code below:
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.lang.reflect.InvocationTargetException;
import javax.swing.*;
#SuppressWarnings("serial")
public class FillingBar2 extends JApplet {
#Override
public void init() {
try {
SwingUtilities.invokeAndWait(new Runnable() {
public void run() {
FillingBarPanel fillingBarPanel = new FillingBarPanel();
add(fillingBarPanel);
add(new JButton(new StartAction(fillingBarPanel)), BorderLayout.PAGE_END);
setSize(getPreferredSize());
}
});
} catch (InvocationTargetException | InterruptedException e) {
System.err.println("Big Problems");
e.printStackTrace();
}
}
}
#SuppressWarnings("serial")
class StartAction extends AbstractAction {
private FillingBarPanel fillingBarPanel;
public StartAction(FillingBarPanel fillingBarPanel) {
super("Start");
putValue(MNEMONIC_KEY, KeyEvent.VK_S);
this.fillingBarPanel = fillingBarPanel;
}
#Override
public void actionPerformed(ActionEvent evt) {
fillingBarPanel.start();
}
}
#SuppressWarnings("serial")
class FillingBarPanel extends JPanel {
private static final int BAR_WIDTH = 20;
private static final int TIMER_DELAY = 100;
private static final int PREF_W = 400;
private static final int PREF_H = 250;
private int filling = 0;
private Timer timer;
public FillingBarPanel() {
timer = new Timer(TIMER_DELAY, new TimerListener());
}
public void start() {
if (timer != null && !timer.isRunning()) {
timer.start();
}
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
int shifting = 0;
for (int i = 0; i < filling / getHeight(); i++) {
shifting = i * BAR_WIDTH;
g.setColor(Color.GREEN);
g.fillRect(shifting, 0, BAR_WIDTH, getHeight());
g.setColor(Color.BLACK);
g.drawRect(shifting, 0, BAR_WIDTH, getHeight());
}
shifting = BAR_WIDTH * (filling / getHeight());
g.setColor(Color.GREEN);
g.fillRect(shifting, getHeight() - (filling % getHeight()), BAR_WIDTH, getHeight());
g.setColor(Color.BLACK);
g.drawRect(shifting, getHeight() - (filling % getHeight()), BAR_WIDTH, getHeight());
}
private class TimerListener implements ActionListener {
#Override
public void actionPerformed(ActionEvent evt) {
filling += 10;
repaint();
}
}
#Override
public Dimension getPreferredSize() {
if (isPreferredSizeSet()) {
return super.getPreferredSize();
}
return new Dimension(PREF_W, PREF_H);
}
}
New question was asked after this one, found here.
I'm new to Java, but I am working on a recreation of "Flappy Bird" to learn more about java and the way that graphics are displayed. Any solutions or suggestions to any of my questions is greatly appreciated. Thanks!
Right now, my program makes a random pipe and scrolls it, but I don't need it to keep scrolling when x1-3 = -83 (this is when the pipe will be off of the screen completely and is no longer needed).
Questions
How can I make my Game.class scroll more than one instance of Pipes.class while adding a preset distance between them? I could find out the distance to put between them, but as far as displaying more than one, I'm not sure how to do that. At most, 3 pipes have to be displayed at the same time.
How can I display a panel for the main menu, and then switch to the pipes panel after a start button is pressed?
Classes
Game.java
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
public class Game {
Pipes panel = new Pipes();
public Game() {
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(panel);
f.setTitle("Pipe Game");
f.setResizable(false);
f.pack();
f.setLocationRelativeTo(null);
f.setVisible(true);
Timer timer = new Timer(10, new ActionListener() { //pipe speed
#Override
public void actionPerformed(ActionEvent e) {
panel.move();
}
});
timer.start();
Timer refresh = new Timer(30, new ActionListener() { //refresh rate
#Override
public void actionPerformed(ActionEvent e) {
panel.repaint();
}
});
refresh.start();
}
public static void main(String args[]) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
new Game();
}
});
}
}
Pipes.java
import java.awt.Dimension;
import java.awt.Graphics;
import javax.swing.JPanel;
public class Pipes extends JPanel {
//Declare and initialiaze variables
int x1 = 754; //xVal start
int x2 = 75; //pipe width
//total width is 83
int y1 = -1; //yVal start
int y2 = setHeightVal(); //pipe height
int gap = 130; //gap height
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
g.clearRect(0,0,750,500); //Clear screen
g.drawRect(x1,y1,x2,y2); //Draw part 1
g.drawRect(x1-3,y2-1,x2+6,25); //Draw part 2
g.drawRect(x1-3,y2+25+gap,x2+6,25); //Draw part 3
g.drawRect(x1,y2+25+gap+25,x2,500-y2-49-gap); //Draw part 4
}
public void move() {
x1--;
}
public int getMyX() { //To determine where the pipe is horizontally
return x1-3;
}
public int getMyY() { //To determine where the pipe is vertically
return y2+25;
}
public int setHeightVal() { //Get a random number and select a preset height
int num = (int)(9*Math.random() + 1);
int val = 0;
if (num == 9)
{
val = 295;
}
else if (num == 8)
{
val = 246;
}
else if (num == 7)
{
val = 216;
}
else if (num == 6)
{
val = 185;
}
else if (num == 5)
{
val = 156;
}
else if (num == 4)
{
val = 125;
}
else if (num == 3)
{
val = 96;
}
else if (num == 2)
{
val = 66;
}
else
{
val = 25;
}
return val;
}
#Override
public Dimension getPreferredSize() {
return new Dimension(751, 501);
}
}
"How can I make my Game.class scroll more than one instance of Pipes.class while adding a preset distance between them? "
Here's some simple logic. You want to use a data structure to hold you pipes. What this data structure will hold is whatever data is required to paint then, like x, y, coordinates. For this task, I prefer just to create a new class with it's own draw method, that I pass the paintComponent's Graphics context to. For example
public class Pipe {
int x;
int y;
public class Pipe(int x, int y) {
this.x = x;
this.y = y;
}
public void drawPipe(Graphics g) {
g.fillRect(x, y, 50, 100);
}
}
Now this is just an example class. The above only draws a rectangle, but this is just to show you what you should be doing.
So next you want to have the data structure to hold three Pipe objects, like an array. I prefer to use a List. You'll want that List in your Pipes class, and add three Pipe object to it. You can specify the x to be anything you like, to keep them the same distance apart
public class Pipes extends JPanel {
List<Pipe> pipes = new ArrayList<Pipe>();
public Pipes() {
pipes.add(new Pipe(50, 100));
pipes.add(new Pipe(150, 100));
pipes.add(new Pipe(250, 100));
}
}
Now in the paintComponent method, all you need to do is loop through them and use its drawPipe method
protected void paintComponent(Graphics g) {
super.paintComponent(g);
for ( Pipe pipe : pipes ){
pipe.drawPipe(g);
}
}
Now you move them all you need to do is move there x positions in the timer, and call repaint. You may also want to check against the x to make sure it doesn't do off the screen, or if you moving them the right, you could put them the the very left then whey go off the screen, like a conveyor belt. So you could do something like this
private static final int X_INC = 5;
...
Timer timer = new Timer(40, new ActionListener(){
public void actionPerformed(ActionEvent e) {
for (Pipe pipe : pipes ){
if (pipe.x >= screenWidth) {
pipe.x = 0;
} else {
pipe.x += X_INC;
}
}
repaint();
}
});
As you can see, what I do is loop through the List and just change all their x coordinates, then repaint(). So you can create your own Pipe class with whatever values you need to paint, and just move them around in the loop.
For the changing of speed, instead of using a hard coded vakue like 10 for the timer, use a variable delay, that you can change like with the click of a button
int delay = 100;
JButton speedUp = new JButton("Speed UP");
JButton slowDown = new JButton("Slow Down");
Timer timer = null;
public Pipes() {
timer = new Timer(delay, new ActionListener(){
...
});
timer.start();
speedUp.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e) {
if (!((delay - 20) < 0)) {
delay -=20;
timer.setDelay(delay);
}
}
});
// do the same for slowDown, but decrease the delay
}
Test this out
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
public class Mario extends JPanel {
private static final int D_W = 800;
private static final int D_H = 300;
private static final int X_INC = 5;
BufferedImage bg;
BufferedImage pipeImg;
List<Pipe> pipes = new ArrayList<>();
int delay = 50;
Timer timer = null;
public Mario() {
try {
bg = ImageIO.read(new URL("http://farm8.staticflickr.com/7341/12338164043_0f68c73fe4_o.png"));
pipeImg = ImageIO.read(new URL("http://farm3.staticflickr.com/2882/12338452484_7c72da0929_o.png"));
} catch (IOException ex) {
Logger.getLogger(Mario.class.getName()).log(Level.SEVERE, null, ex);
}
pipes.add(new Pipe(100, 150, pipeImg));
pipes.add(new Pipe(400, 150, pipeImg));
pipes.add(new Pipe(700, 150, pipeImg));
timer = new Timer(delay, new ActionListener(){
public void actionPerformed(ActionEvent e) {
for (Pipe pipe : pipes) {
if (pipe.x > D_W) {
pipe.x = 0;
} else {
pipe.x += X_INC;
}
}
repaint();
}
});
timer.start();
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawImage(bg, 0, 0, getWidth(), getHeight(), this);
for (Pipe pipe : pipes) {
pipe.drawPipe(g);
}
}
#Override
public Dimension getPreferredSize() {
return new Dimension(D_W, D_H);
}
public class Pipe {
int x;
int y;
Image pipe;
public Pipe(int x, int y, Image pipe) {
this.x = x;
this.y = y;
this.pipe = pipe;
}
public void drawPipe(Graphics g) {
g.drawImage(pipe, x, y, 75, 150, Mario.this);
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
JFrame frame = new JFrame("Mario Pipes");
frame.add(new Mario());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
}
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);
}
}
}