I'm trying to write a simple program: a bouncing ball that appears and starts bouncing after you press the "Start" button on the screen. The program should be closed by pressing "X".
For some reason, it runs very slowly. The ball is blinking, and I have to wait for a long time after I press the "X" for program to close.
Here is the code:
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.util.*;
import javax.swing.*;
public class Bounce
{
public static void main(String[] args)
{
JFrame frame = new BounceFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.show();
}
}
class BounceFrame extends JFrame
{
public BounceFrame()
{
setSize(WIDTH, HEIGHT);
setTitle("Bounce");
Container contentPane = getContentPane();
canvas = new BallCanvas();
contentPane.add(canvas, BorderLayout.CENTER);
JPanel buttonPanel = new JPanel();
addButton(buttonPanel, "Start", new ActionListener()
{
public void actionPerformed(ActionEvent evt)
{
addBall();
}
});
contentPane.add(buttonPanel, BorderLayout.SOUTH);
}
public void addButton(Container c, String title, ActionListener listener)
{
JButton button = new JButton(title);
c.add(button);
button.addActionListener(listener);
}
public void addBall()
{
try
{
Ball b = new Ball(canvas);
canvas.add(b);
for (int i = 1; i <= 10000; i++)
{
b.move();
Thread.sleep(10);
}
}
catch (InterruptedException exception)
{
}
}
private BallCanvas canvas;
public static final int WIDTH = 300;
public static final int HEIGHT = 200;
}
class BallCanvas extends JPanel
{
public void add(Ball b)
{
balls.add(b);
}
public void paintComponent(Graphics g)
{
super.paintComponent(g);
Graphics2D g2 = (Graphics2D)g;
for (int i = 0; i < balls.size(); i++)
{
Ball b = (Ball)balls.get(i);
b.draw(g2);
}
}
private ArrayList balls = new ArrayList();
}
class Ball
{
public Ball(Component c) { canvas = c; }
public void draw(Graphics2D g2)
{
g2.fill(new Ellipse2D.Double(x, y, XSIZE, YSIZE));
}
public void move()
{
x += dx;
y += dy;
if (x < 0)
{
x = 0;
dx = -dx;
}
if (x + XSIZE >= canvas.getWidth())
{
x = canvas.getWidth() - XSIZE;
dx = -dx;
}
if (y < 0)
{
y = 0;
dy = -dy;
}
if (y + YSIZE >= canvas.getHeight())
{
y = canvas.getHeight() - YSIZE;
dy = -dy;
}
canvas.paint(canvas.getGraphics());
}
private Component canvas;
private static final int XSIZE = 15;
private static final int YSIZE = 15;
private int x = 0;
private int y = 0;
private int dx = 2;
private int dy = 2;
}
The slowness comes from two related problems, one simple and one more complex.
Problem #1: paint vs. repaint
From the
JComponent.paint docs:
Invoked by Swing to draw components.
Applications should not invoke paint directly, but should instead use the repaint method to schedule the component for redrawing.
So the canvas.paint() line at the end of Ball.move must go.
You want to call
Component.repaint
instead...
but just replacing the paint with repaint will reveal the second problem, which prevents the ball from even appearing.
Problem #2: Animating inside the ActionListener
The ideal ActionListener.actionPerformed method changes the program's state and returns as soon as possible, using lazy methods like repaint to let Swing schedule the actual work for whenever it's most convenient.
In contrast, your program does basically everything inside the actionPerformed method, including all the animation.
Solution: A Game Loop
A much more typical structure is to start a
javax.swing.Timer
when your GUI starts, and just let it run
"forever",
updating your simulation's state every tick of the clock.
public BounceFrame()
{
// Original code here.
// Then add:
new javax.swing.Timer(
10, // Your timeout from `addBall`.
new ActionListener()
{
public void actionPerformed(final ActionEvent ae)
{
canvas.moveBalls(); // See below for this method.
}
}
).start();
}
In your case, the most important
(and completely missing)
state is the
"Have we started yet?"
bit, which can be stored as a boolean in BallCanvas.
That's the class that should do all the animating, since it also owns the canvas and all the balls.
BallCanvas gains one field, isRunning:
private boolean isRunning = false; // new field
// Added generic type to `balls` --- see below.
private java.util.List<Ball> balls = new ArrayList<Ball>();
...and a setter method:
public void setRunning(boolean state)
{
this.isRunning = state;
}
Finally, BallCanvas.moveBalls is the new
"update all the things"
method called by the Timer:
public void moveBalls()
{
if (! this.isRunning)
{
return;
}
for (final Ball b : balls)
{
// Remember, `move` no longer calls `paint`... It just
// updates some numbers.
b.move();
}
// Now that the visible state has changed, ask Swing to
// schedule repainting the panel.
repaint();
}
(Note how much simpler iterating over the balls list is now that the list has a proper generic type.
The loop in paintComponent could be made just as straightforward.)
Now the BounceFrame.addBall method is easy:
public void addBall()
{
Ball b = new Ball(canvas);
canvas.add(b);
this.canvas.setRunning(true);
}
With this setup, each press of the space bar adds another ball to the simulation.
I was able to get over 100 balls bouncing around on my 2006 desktop without a hint of flicker.
Also, I could exit the application using the 'X' button or Alt-F4, neither of which responded in the original version.
If you find yourself needing more performance
(or if you just want a better understanding of how Swing painting works),
see
"Painting in AWT and Swing:
Good Painting Code Is the Key to App Performance"
by Amy Fowler.
I would suggest you to use 'Timer' class for running your gameloop.It runs infinitely and you can stop it whenever you want using timer.stop()
You can also set its speed accordingly.
Related
so i'm trying to make it where if these two shapes touch each other the window closes. Here is the first part
public class Mayflower {
JFrame f = new JFrame();
public static void main(String[] args) {
Mayflower bob = new Mayflower();
bob.Start();
}
private void Start(int clothes, int food, int repair, int money) {
int complete = 0;
Mayflower bob = new Mayflower();
//JOptionPane.showMessageDialog(null, "Your equipment:\nClothes - " + clothes + "\nFood - " + food + "\nrepair equipment - " + repair + "\nMoney left - $" + money);
bob.epic(complete);
}
public void epic(int complete) {
if (complete == 0){
Iceberg Tim = new Iceberg();
f.add(Tim);
f.setVisible(true);
f.setSize(600, 600);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setTitle("SAILIN BABEEEEY");
f.setLocation(600, 200);
}
if(complete == 1){
System.out.println("odeyladoeijoo");
f.dispose();
}
}
}
Then it calls to the constructor iceberg where the minigame is, I deleted all the movement input because it wasn't relevant:
package mayflower;
public class Iceberg extends JPanel implements ActionListener, KeyListener {
Timer time = new Timer(5, this);
int x = 260;
int y = 500;
int velx = 0;
int vely = 0;
int hitscany = -4000;
int hitscanvely = -1;
public Iceberg() {
time.start();
addKeyListener(this);
setFocusable(true);
setFocusTraversalKeysEnabled(false);
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
g.setColor(MyColor1);
g.fillRect(x, y, 40, 60);
g.setColor(Color.GRAY);
g.fillRect(0, hitscany, 650, 0);
if (y == hitscany) {
int complete = 1;
Mayflower bob = new Mayflower();
bob.epic(complete);
}
time.start();
}
So i made it to where The "hitscan" object moves down the screen and when it touches the object the window is supposed to close. When my if statement (for if the y coordinates of the two objects are equal) calls the public void epic its supposed to "activate" the if statement for if complete is == 1 and dispose of the frame but for some reason it doesn't
So, I assume that, this (previously, now removed) code goes some where in your Iceberg key handler code...
if ((((x - icex)) >= -40 && ((x - icex) - 180) <= -130) && (((y - icey)) >= -60 && ((y - icey) - 180) <= -130)) {
int complete = 1;
Mayflower bob = new Mayflower();
bob.epic(complete);
}
This highlights a number of issues. First, you are creating another instance of Mayflower, which is creating another instance of JFrame, which is what's getting disposed, not the original frame.
Iceberg really has no need to interact with Mayflower, it's beyond it's realm of responsibility. Instead, Iceberg "should" be generating event notifications to interested parties about its change in state.
For that, we need an observer pattern!
Let's start with a simple interface which describes all the notifications Iceberg is willing to make...
public interface GameListener {
public void completed(Iceberg berg);
}
Next, we need some way to manage these listeners in Iceberg...
public class Iceberg extends JPanel implements ActionListener, KeyListener {
private List<GameListener> listeners = new ArrayList<>(25);
public void addGameListener(GameListener listener) {
listeners.add(listener);
}
public void removeGameListener(GameListener listener) {
listeners.remove(listener);
}
And finally, some way to generate the notifications...
public class Iceberg extends JPanel implements ActionListener, KeyListener {
//...
protected void fireCompleted() {
for (GameListener listener : listeners) {
listener.completed(this);
}
}
Now, when you have a "completed" state, you can notify the interested parties...
if ((((x - icex)) >= -40 && ((x - icex) - 180) <= -130) && (((y - icey)) >= -60 && ((y - icey) - 180) <= -130)) {
fireCompleted();
}
Now, in your start method, you simply need to create an instance of Iceberg, register a GameListener and get it all started...
private void Start(int clothes, int food, int repair, int money) {
Iceberg Tim = new Iceberg();
Tim.addGameListener(new GameListener() {
#Override
public void completed(Iceberg berg) {
f.dispose();
}
});
f.add(Tim);
f.setVisible(true);
f.setSize(600, 600);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setTitle("SAILIN BABEEEEY");
f.setLocation(600, 200);
}
Observations...
Okay, there is plenty about your code sample to be worried about, but let's start with...
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
g.setColor(Color.RED);
g.fillRect(x, y, 40, 60);
g.setColor(Color.GRAY);
g.fillRect(0, hitscany, 650, 0);
if (y == hitscany) {
int complete = 1;
Mayflower bob = new Mayflower();
bob.epic(complete);
}
time.start();
}
paintComponent should never be public, no-one should ever be calling it directly.
You declare but never use g2
This...
if (y == hitscany) {
int complete = 1;
Mayflower bob = new Mayflower();
bob.epic(complete);
}
is a bad idea on a number of levels. Paint should paint the current state of the component, nothing else, it should not be making decisions about the state of the component. This belongs in your main loop
And...
time.start();
I can't begin to tell you how horrible this is. paintComponent will be called often (if you're performing animation), meaning you are continuously reseting the Timer. The Timer's state should never be modified inside paintComponent. Instead, it should control through other means, like the constructor or start/stop methods
KeyListener is a poor choice now days. It suffers from a number of well known and documented short comings. A better, all round solution, is to use the Key Bindings API which has been designed to help solve these issues in a reliable and robust way
You could use
f.setVisible(false)
This just hides the window, and f.dispose() deletes the actual object.
If you want it to act like you clicked the X button, then use this:
f.dispatchEvent(new WindowEvent(frame, WindowEvent.WINDOW_CLOSING));
(got from How to programmatically close a JFrame)
(f being your JFrame)
I am making a classic space shooter type game for a GUI project, using JPanels and swing etc. I have a multi-layered main menu with a button for play and exit etc, however, the issue I am having is in my DrawPanel class which paints all the graphics for the game. This class has one giant paint component method that activates on a timer every 500 ms, and renders blinking stars on a background, renders the spaceship, and renders falling enemies. The stars are blinking, the enemies are falling but the spaceship is not moving at all and I have to use the "a" and "d' keys. The question: how can I move the ship? Below is an extremely summarized version of that rendering class to keep it simple, the paint component class is full of other things however including generating blinking stars.
public DrawPanel(int x, int y)
{
t = new Timer(500, new ActionListener(){#Override public void actionPerformed (ActionEvent event){repaint();}});
this.addKeyListener(new KeyAdapter()
{
#Override public void keyPressed(KeyEvent e) { ship.keyPressed(e);System.out.println("keypressed");}
#Override public void keyReleased(KeyEvent e) { ship.keyReleased(e);System.out.println("keyreleased");}
});
t.start();
}
public void paintComponent(Graphics Graphic)
{
super.paintComponent(Graphic);
drawShip(Graphic);
drawEnemy(Graphic,10);
}
private void drawShip(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
g2d.drawImage(ship.getImage(), ship.getX(),
ship.getY(), this);
}
This is the spaceship class that is called by the drawship method, and that explains the calls by variable "ship" in the previous code.(Note: there are no compiler errors or runtime errors in anything)
public class Spaceship {
private int x = 780;
private int y = 850;
private Image image;
public Spaceship()
{
ImageIcon ii = new ImageIcon("spaceship.png");
image = ii.getImage();
}
public void keyPressed(KeyEvent e) {
int key = e.getKeyCode();
if (key ==KeyEvent.VK_A) {
x += -10;
}
if (key == KeyEvent.VK_D) {
x+= 10;
}
}
public void keyReleased(KeyEvent e)
{
int key = e.getKeyCode();
if (key == KeyEvent.VK_A) {
x += 0;
}
if (key == KeyEvent.VK_D) {
x += 0;
}
}
}
My thought process on this was that the keyboard would be active at all times and anytime it registered a press it would modify an x so that the next time the ship would be drawn it would be in a different position. However it would be bound by the same time that the enemies and background are bound by and wouldn't fluidly move side to side now that I think about it, but I have no clue how to make a separate paint component, timer, and repaint as that's the only way I know to paint. Also
I do realize I haven't done bound checking on this for the ship movement but that shouldn't be an issue right now as it's not even moving a centimeter yet, much less out of bounds, and it is not even printing the debug statements in the listener.
All of my classes are added to panels which have been bound to buttons and tabs in my main menu by adding it all to default constructor of the main method, so this is how the entire game executes:
public static void main(String[] args) throws IOException
{
GalagaRipOff main = new GalagaRipOff();
}
I testing to implement graphics into MVC structure but Im a bit stuck. Here is what I got so far. For now I just want to get the red ball to bounce back and forth. And use the button start to start the thread and button stop to stop the thread that runs the GameLoop in the controller.
But I think Im mixing this up a bit. Would very much appreciate some feedback!
Heres what I got so far:
GameModell
suppose to controll the bouncing. If the location of the ball is under 40 px or above 80 px - multiply the locationX with -1 to make the ball change direction
GameView
Here Im putting the labels on a JFrame. I also want to display the buttons start and stop to controll the thread but I guess they are hidden by the JPanel in TheGraphics class
GameController
Starts and stops the thread with ActionListeners. Contains the GameLoop
TheGraphics
Paints the ball and controll the direction
I guess I got a lot of thing that are all wrong but this is the best I can do at the moment. Would very much apreciate some help!
Thanks!
MAIN:
public class MVCgame {
public static void main(String[] args) {
GameModel gm = new GameModel();
GameView gv = new GameView();
GameController gc = new GameController(gm, gv);
}
}
MODEL:
public class GameModel {
private int multi = 1;
public void setMulti(int locX) {
if(locX < 40 || locX > 80) {
multi = multi * -1;
}
}
public int multi() {
return multi;
}
}
VIEW:
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class GameView extends JFrame {
private JPanel jp = new JPanel();
private JButton start = new JButton("Start");
private JButton stop = new JButton("Stop");
TheGraphics gr = new TheGraphics();
public GameView() {
add(jp);
add(gr);
jp.add(start);
jp.add(stop);
setSize(250, 250);
setVisible(true);
}
public void addListener(ActionListener theListener) {
start.addActionListener(theListener);
stop.addActionListener(theListener);
}
public JButton getStart() {
return start;
}
public JButton getStop() {
return stop;
}
// GUESS I SHOULD PUT THIS IN THE VIEW???
public void paintEllipse(Graphics theG) {
Graphics2D g2d = (Graphics2D) theG;
g2d.setColor(new Color(255, 0, 0));
g2d.fillOval(0, 0, 10, 10);
}
}
CONTROLLER:
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class GameController implements Runnable {
GameView gv;
GameModel gm;
private Thread thread;
private boolean running = false;
public GameController(GameModel gm, GameView gv) {
this.gv = gv;
this.gm = gm;
gv.addListener(theListener);
start();
}
ActionListener theListener = new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (e.getSource() == gv.getStart()) {
start();
System.out.println("PLAY = ");
} else if (e.getSource() == gv.getStop()) {
stop();
System.out.println("STOP = ");
}
}
};
public synchronized void start() {
thread = new Thread(this);
thread.start();
running = true;
}
public synchronized void stop() {
thread.interrupt();
running = false;
}
// GameLoop
public void run() {
long lastTime = System.nanoTime();
double amountOfTicks = 60.0;
double ns = 1000000000 / amountOfTicks;
double delta = 0;
long timer = System.currentTimeMillis();
int frames = 0;
while (running) {
long now = System.nanoTime();
delta += (now - lastTime) / ns;
lastTime = now;
while (delta >= 10) {
// tick();
delta--;
// repainting the graphics
gv.gr.drawer();
gm.setMulti(gv.gr.drawer());
System.out.println("gv.gr.drawer() = " + gv.gr.drawer() + " gm.multi() " + gm.multi());
// I want to use this value in the model to change the direction
}
if (running) {
}
frames++;
if (System.currentTimeMillis() - timer > 1000) {
timer += 1000;
System.out.println("FPS: " + frames);
frames = 0;
}
}
}
}
THE GRAPHICS:
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.JPanel;
public class TheGraphics extends JPanel {
private int locX = 40;
public TheGraphics() {
}
public int drawer() {
locX++;
repaint();
return locX;
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setColor(new Color(255, 0, 0));
g2d.fillOval(locX, 30, 10, 10);
}
}
GameModell suppose to controll the bouncing. If the location of the ball is under 40 px or above 80 px - multiply the locationX with -1 to make the ball change direction
public void setMulti(int locX) {
if(locX < 40 || locX > 80) {
multi = multi * -1;
}
}
Really bad idea. You should always check position and direction (sign(speed)). Otherwise, your object might get stuck out of bounds always changing direction without moving from place forever.
Apart from this, using the MVC concept is overkill in my eyes and shouldn't be used in such a small project nor in a game. In a game, you should more or less put all three together. Of course you can, but the advantages and disadvantages of the MVC concept don't fit the needs of a game in many ways (except for the GUI, perhaps).
Your main loop might look something like this (you kind of did this already, but why is the tick() commented out in your code?):
while (running) {
update(); // Update all game objects
paint(); // Paint them all
}
Each game object will have its own update() and paint() implementation. You absolutely need to separate the logic of update and paint, even if they are in the same class. So this one:
public int drawer() {
locX++;
repaint();
return locX;
}
is an absolute no-go.
Edit: (Referring your update answer)
You are using the method location() for different purposes. According to the Java name convention, you should rename it getLocation() and setLocation() depending on the use to clarify the code.
(Even if this is not really MVC anymore, I'd let GameFrame implement ActionListener instead of specifying it as variable of GameController.)
One thing you should really change is this one:
private int locX = 0;
public void location(int loc) {
this.locX = (int) loc;
}
Basically, you are duplicating the location value every frame and create unused redundant data. Another problem is, that this might work fine for only one variable, but what if you add more than the position to your model later on? Instead TheGraphics has to render on an instance of the data model, not its values. As long you are using one GameModel
private GameModel model; // set value once at initialisation
and rendering its values in paintComponent will work fine, but if you want to add more than one GameModel (handling GameModel more like a GameObjectModel), you will need to pass it as parameter in the paint method.
public void update() {
repaint();
}
Remove it and try getting around without. A method called from one place forwarding to a different method is a bad idea most of the time, especially if it obfuscates the functionality with a different name.
gv.gr.update();
gv.gr.location(gm.location());
You are first repainting your image and then setting the location? Basically, your game runs one frame behind all the time. Swap that order.
gv.gr.location(gm.location());
gv.gr.repaint();
Will be fine (I already said about location()).
I am making a simple game project and I am having a problem when trying to create a rectangle that moves across the screen.
Here is the main class:
`public class Main extends Canvas implements Runnable {
private static final long serialVersionUID = 1L;
private JFrame frame;
boolean running = false;
Graphics g;
static int HEIGHT = 500;
static int WIDTH = HEIGHT * 16 / 9;
SoundHandler sh = new SoundHandler();
//Game state manager
private GameStateManager gsm;
public Main()
{
//window
frame = new JFrame("Game");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(this, BorderLayout.CENTER);
frame.pack();
frame.setSize(WIDTH, HEIGHT);
frame.setResizable(false);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
init();
}
void init()
{
gsm = new GameStateManager();
sh.playMusic("Undertale.wav", 1);
}
public synchronized void start(){
running = true;
new Thread(this).start();
}
public synchronized void stop(){
running = false;
}
//game loop
public void run()
{
//init time loop variables
long lastLoopTime = System.nanoTime();
final int TARGET_FPS = 60;
final long OPTIMAL_TIME = 1000000000 / TARGET_FPS;
double lastFpsTime = 0;
int fps = 0;
while(running)
{
//work out how long its been since last update
//will be used to calculate how entities should
//move this loop
long now = System.nanoTime();
long updateLength = now - lastLoopTime;
lastLoopTime = now;
double delta = updateLength / ((double)OPTIMAL_TIME);
//update frame counter
lastFpsTime += updateLength;
fps++;
//update FPS counter
if(lastFpsTime >= 1000000000)
{
System.out.println("FPS " + fps);
lastFpsTime = 0;
fps = 0;
}
//game updates
update(delta);
//draw
draw(g);
try{
Thread.sleep((lastLoopTime - System.nanoTime() + OPTIMAL_TIME)/1000000 );
}catch(InterruptedException e){
System.out.println("Error in sleep");
}
}
}
private void update(double delta)
{
//updates game state code
gsm.update(delta);
}
public void draw(Graphics g)
{
gsm.draw(g);
}`
here is the class I want to draw the rectangle with
package me.mangodragon.gamestate;
import java.awt.Graphics;
public class MainState extends GameState{
int x;
public MainState(GameStateManager gsm){
this.gsm = gsm;
}
public void init() {
}
public void update(double delta) {
x += 2 * delta;
}
public void draw(Graphics g) {
g.drawRect(x, 0, 50, 50);
g.dispose();
}
public void keyPressed(int k) {
}
public void keyReleased(int k) {
}
}
I keep getting this error:
Exception in thread "Thread-4" java.lang.NullPointerException
at me.mangodragon.gamestate.MainState.draw(MainState.java:22)
at me.mangodragon.gamestate.GameStateManager.draw(GameStateManager.java:37)
at me.mangodragon.main.Main.draw(Main.java:118)
at me.mangodragon.main.Main.run(Main.java:100)
at java.lang.Thread.run(Unknown Source)
I tried to fix it, but I could not locate the problem.
Thanks!
You never assign anything to g (Graphics). Now, before you run off and try and figure out how you might do that, I'd highly, highly recommend you get rid of this variable, it's going to cause you too many issues.
Normally, when the system wants your component to painted, it calls your paint method and passes you the Graphics context which it wants you to paint to. This approach is known as passive painting, as the paint requests come at random times, which isn't really what you want. Another issue is java.awt.Canvas isn't double buffered, which will cause flickering to occur as your component is updated.
You might want to take a look at Painting in AWT and Swing and Performing Custom Painting for more details
You could use a JPanel instead, which is double buffered, but the main reason for using java.awt.Canvas is so you can make use the BufferStrategy API. This not only provides double buffering, but also provides you with a means by which you can take direct control over the painting process (or active painting).
See BufferStrategy and BufferStrategy and BufferCapabilities for more details
You defined g as such:
Graphics g;
But never gave it a value.
This is not how you should be drawing shapes, anyways. Instead, override the paint method (inherited from Canvas) in class Main:
#Override
public void paint(Graphics2D g) {
//Drawing code goes in here. This runs whenever the Canvas is rendered.
}
Then, when you want to update it, such as in your while loop, run
this.repaint(); //note that this doesn't take arguments
If you want to use the draw(Graphics g) method in the other class, call it in paint().
public void paint(Graphics2D g) {
gsm.draw(g);
}
The problem is that you have not defined g, so it is null. And for the most part, you are never supposed to create a new Graphics object, but instead get it from somewhere.
Since you are inheriting a Canvas, this can be very easily done.
First, you should change your draw method to be like this.
private void draw() {
BufferStrategy bs = this.getBufferStrategy();
if (bs == null) {
this.createBufferStrategy(3);
return;
}
Graphics g = bs.getDrawGraphics();
// Draw your game here, using the g declared above
g.dispose();
bs.show();
}
The first few lines create something called a BufferStrategy which you can read more about here but it essentially lets Java render the next couple frames ahead of schedule so that you don't see any flickering.
From the BufferStrategy, you can get the Graphics object to draw on.
And, finally, you have to dispose of the Graphics object, and then show the Buffer so that everything you did shows on the screen.
I am working on a keylistener exercise for my java class, but have been stuck for the past week. I appreciate any helpful suggestions. The exercise is:
"Write a program that draws line segments using the arrow keys. The
line starts from the center of the frame and draws toward east, north,
west, or south when the right-arrow key, up-arrow key, left-arrow key,
or down-arrow key is clicked."
Through debugging I figured out that the KeyListener works to the point
of getting to drawComponent(Graphics g), but it only draws when I press
down or right and that only works the first couple times. Here is my code:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
#SuppressWarnings("serial")
public class EventProgrammingExercise8 extends JFrame {
JPanel contentPane;
LinePanel lines;
public static final int SIZE_OF_FRAME = 500;
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
try {
EventProgrammingExercise8 frame = new EventProgrammingExercise8();
frame.setVisible(true);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
public EventProgrammingExercise8() {
setTitle("EventExercise8");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(SIZE_OF_FRAME, SIZE_OF_FRAME);
contentPane = new JPanel();
lines = new LinePanel();
contentPane.add(lines);
setContentPane(contentPane);
contentPane.setOpaque(true);
lines.setOpaque(true);
lines.setFocusable(true);
lines.addKeyListener(new ArrowListener());
}
private class LinePanel extends JPanel {
private int x;
private int y;
private int x2;
private int y2;
public LinePanel() {
x = getWidth() / 2;
y = getHeight() / 2;
x2 = x;
y2 = y;
}
protected void paintComponent(Graphics g) {
g.drawLine(x, y, x2, y2);
x = x2;
y = y2;
}
public void drawEast() {
x2 += 5;
repaint();
}
public void drawWest() {
x2 -= 5;
repaint();
}
public void drawNorth() {
y2 -= 5;
repaint();
}
public void drawSouth() {
y2 += 5;
repaint();
}
}
private class ArrowListener extends KeyAdapter {
public void keyPressed(KeyEvent e) {
int key = e.getKeyCode();
if (key == KeyEvent.VK_RIGHT) {
lines.drawEast();
} else if (key == KeyEvent.VK_LEFT) {
lines.drawWest();
} else if (key == KeyEvent.VK_UP) {
lines.drawNorth();
} else {
lines.drawSouth();
}
}
}
}
Thanks.
A few things jump out at me...
public LinePanel() {
x = getWidth() / 2;
y = getHeight() / 2;
This will be an issue, because at the time you construct the class, it's size is 0x0
Apart from the fact that you haven't called super.paintComponent which breaks the paint chain, you seem to think that painting is accumaltive...
protected void paintComponent(Graphics g) {
g.drawLine(x, y, x2, y2);
x = x2;
y = y2;
}
Painting in Swing is destructive. That is, you are expected to erase to the Graphics context and rebuild the output from scratch. The job paintComponent is to clear the Graphics context ready for painting, but you've not called super.paintComponent, breaking the paint chain and opening yourself up to a number of very ugly paint artifacts
Calling setSize(SIZE_OF_FRAME, SIZE_OF_FRAME); on a frame is dangerous, as it makes no guarantee about the frames border insets, which will reduce the viewable area available to you.
This....
contentPane = new JPanel();
lines = new LinePanel();
contentPane.add(lines);
setContentPane(contentPane);
Is not required, it just adds clutter to your code. It's also a good hint as to what is going wrong with your code.
JPanel uses a FlowLayout by default. A FlowLayout uses the component's preferred size to determine how best to layout the components. The default preferred size of a component is 0x0
You could use...
lines = new LinePanel();
add(lines);
instead or set the contentPane to use a BorderLayout which will help...
Try adding lines.setBorder(new LineBorder(Color.RED)); add see what you get...
Oddly, during my testing, your KeyListener worked fine...
Basically...
Override the getPreferredSize method of the LinePanel and return the size of the panel you would like to use.
Use a java.util.List to maintain a list of Points that need to be painted.
In you paintComponent method, use the Point List to actually render you lines. This will be a bit tricky, as you need two points and the List may contain an odd number of points, but's doable.
Calculate the start Point either by using the preferred size or some other means (like using a ComponentListener and monitoring the componentResized method. This becomes tricky as your component may be resized a number of times when it is first created and released to the screen and you will want to ignore future events once you have your first point)