I am trying to do a PAC-MAN clone in Java. I am using swing and awt for the GUI.
I implemented motion into my main character and it responds to keys pressed, but when the character moves the "old image" stays there. So, rather than it looking like pacman moves through the screen he leaves a trail. It is my understanding that when I use the repaint() function the image should be cleared and painted again.
This is my code:
//PACMAN CLASS
import Entities.Ghost;
import Entities.Player;
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
public class Pacman extends JPanel implements KeyListener {
Player player = new Player("Pacman.png", 0, 0);
Ghost[] ghosts = new Ghost[4];
public Pacman(){
addKeyListener(this);
setFocusable(true);
ghosts[0] = new Ghost("Ghost_Red.png", 200, 200);
ghosts[1] = new Ghost("Ghost_Red.png", 150, 150);
ghosts[2] = new Ghost("Ghost_Red.png", 300, 100);
ghosts[3] = new Ghost("Ghost_Red.png", 50, 300);
}
public void paintComponent(Graphics g){
player.draw(g, this);
for(Ghost ghost: ghosts){
ghost.draw(g, this);
}
}
public void update() {
repaint();
}
#Override
public void keyTyped(KeyEvent e) {
}
#Override
public void keyPressed(KeyEvent e) {
if(e.getKeyCode() == KeyEvent.VK_ENTER){
new Thread( () -> {
while (true){
update();
try{
Thread.sleep(10);
}catch(InterruptedException err){
err.printStackTrace();
}
}
}).start();
}
int SPEED = 4;
if(e.getKeyCode() == KeyEvent.VK_RIGHT && player.x < (getWidth() - player.width)){
player.x += SPEED;
}
if(e.getKeyCode() == KeyEvent.VK_LEFT && player.x > 0){
player.x -= SPEED;
}
if(e.getKeyCode() == KeyEvent.VK_UP && player.y > 0){
player.y -= SPEED;
}
if(e.getKeyCode() == KeyEvent.VK_DOWN && player.y < (getHeight() - player.height)){
player.y += SPEED;
}
}
#Override
public void keyReleased(KeyEvent e) {
}
}
//MY MAIN
import javax.swing.*;
public class Main {
public static void main(String[] args) {
JFrame frame = new JFrame("Pacman");
Pacman panel = new Pacman();
frame.getContentPane().add(panel);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
frame.setSize(224*2,288*2);
frame.setResizable(false);
}
}
I followed a tutorial to get here and the guy's graphic do work properly. Thoug he is using JAva 10 and I am using Java 16
When doing custom painting, and especially when doing some "animations", it's really important to call super.paintComponent(g); as the first line in your paintComponent(...) method
This will repaint all the things you're not painting on that "frame", and thus this is what you need to do to solve your problem.
If you want to know more in detail what super.paintComponent() does, then read this answer.
So your code should end up looking like this:
public void paintComponent(Graphics g) {
super.paintComponent(g);
player.draw(g, this);
for(Ghost ghost : ghosts) {
ghost.draw(g, this);
}
//Any extra painting do it here
}
Also, this line:
frame.setVisible(true);
Should be the last one on your program
And about this line:
frame.setSize(224*2,288*2);
Better override your JPanel's getPreferredSize, and then call frame.pack() this will make your pane to have that size and then add the frame decorations, otherwise your panel will be smaller than you think it is; for more information take a look at this question and answers: Should I avoid the use of set(Preferred|Maximum|Minimum)Size methods in Java Swing?
And as a tip, don't use "magic numbers", instead declare constants as of what those 224 and 288 means and why you multiply them by 2.
And forgot to mention that when programming games, it's better to use KeyBindings rather than infinite loops (while(true)) with KeyListeners; here's an excellent answer from #HovercraftFullOfEels that shows how to do it.
Related
I'm trying to make Pong with Java and Swing. However, I have two problems - one, the rectangle on-screen doesn't move at all, and two, a NullPointerException is happening even though the code still runs. Here are my two files:
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.WindowEvent;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class Pong extends JFrame implements KeyListener {
private static final long serialVersionUID = -5782301423436L;
JPanel panel;
Paddle paddle1;
public Pong() {
super("Pong");
panel = new JPanel();
this.add(panel);
super.setPreferredSize(new Dimension(800, 600));
super.setVisible(true);
super.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
super.pack();
addKeyListener(this);
paddle1 = new Paddle(100, 300);
}
public static void main(String[] args) {
#SuppressWarnings("unused")
Pong game = new Pong();
}
#Override
public void paint(Graphics g) {
super.paint(g);
g.setColor(Color.WHITE);
g.fillRect(0, 0, 800, 600);
g.setColor(Color.BLACK);
this.paddle1.draw(g);
}
public void keyTyped(KeyEvent e) {}
public void keyReleased(KeyEvent e) {}
// I've tested this part and the w and s key presses
// are still detected, but nothing happens. Why not?
public void keyPressed(KeyEvent keyEvent) {
int key = keyEvent.getKeyCode();
if (key == KeyEvent.VK_ESCAPE) {
this.dispatchEvent(new WindowEvent(this, WindowEvent.WINDOW_CLOSING));
} else if (key == KeyEvent.VK_W) {
this.paddle1.up();
} else if (key == KeyEvent.VK_S) {
this.paddle1.down();
}
}
}
import java.awt.Graphics;
public class Paddle {
// Position of the paddle's center
int x;
int y;
public Paddle(int x, int y) {
this.x = x;
this.y = y;
}
public void draw(Graphics g) {
g.fillRect(this.x - 10, this.y - 40, 20, 80);
}
// Up is negative because Java coordinates
public void up() {
this.y -= 30;
}
public void down() {
this.y += 30;
}
}
I've tested the keyPressed method in the Pong file, and when the w and s keys are pressed, it does, in fact, detect them (it will print a line out to the console). I'm very new to graphics in Java, and this is the first thing I've tried creating that's not copy-pasted from a tutorial on YouTube. I've been looking at my code for about an hour and can't find where the error could possibly be. Any and all help would be greatly appreciated.
I've checked the code and can help you solve the errors.
First, here you've shown the Paddle using paint() method of the JFrame Pong. You're detecting the 'W' and 'S' keys and according to the key you've written the code to update the y co-ordinate in the Paddle class.
But, the problem here is, you're just updating the y co-ordinate. You're not drawing the paddle with new co-ordinates again. So, after setting the new y value, you should draw the paddle again.
So, in the keyPressed() method, after
this.paddle1.up();
and
this.paddle1.down();
you should again call the draw() method of the Paddle class that will draw the Paddle at the modified location.
In this draw() method you're passing a Graphics object which is the graphics configuration of the current JFrame Pong. So, this can be get using getGraphics() method of the JFrame.
So, the line to add after up() or down() will be
this.paddle1.draw(getGraphics());
Since we are dealing with graphics, it is also the proper thing to repaint the JFrame once you change something in its graphics. If you have added only the above line after up() and down() method in the keyPressed() method, you will see the Paddle is not moving. Its previous drawing will be as it is and as per the key pressed, new Paddle will be drawn upwards or downwards. So, instead of a moving paddle you will see a vertical bar. This is happening because we are modifying the graphics but not painting the JFrame again. So, it will just add the new rectangle in the JFrame keeping previous as it is.
So, to get rid of this, you should use repaint() method of the JFrame so that it draws only the latest screen.
So, the complete keyPressed() method would be like :
#Override
public void keyPressed(KeyEvent keyEvent) {
int key = keyEvent.getKeyCode();
if (key == KeyEvent.VK_ESCAPE) {
this.dispatchEvent(new WindowEvent(this, WindowEvent.WINDOW_CLOSING));
} else if (key == KeyEvent.VK_W) {
this.paddle1.up();
this.paddle1.draw(getGraphics());
repaint();
} else if (key == KeyEvent.VK_S) {
this.paddle1.down();
this.paddle1.draw(getGraphics());
repaint();
}
}
So, now after detecting correct keys, it is drawing the paddle again and
after that discards the previous ones as well so that while you will run this code, you will be able to see the paddle is moving as per the key pressed.
Now, about the NullPointerException. I tried to run this code but I could not reproduce such exception. So, after trying the suggested code, if you encounter NPE, you can update the stack trace to get it solved.
I have read the comments and they are very useful. Do check the link that explains better way for listening to the key events.
Thank you.
I created this java program and I wanted an output of which if int x and int y are above 100, it would draw a rectangle. But it doesn't. How can I make it work?Is there another line of code I need to add?
Here's my code:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
#SuppressWarnings("serial")
public class GameSetup extends JPanel implements MouseMotionListener{
public static JFrame njf = new JFrame("Test");
public static int x, y;
public static void main(String[] args){
GameSetup gs = new GameSetup();
njf.add(gs);
}
public void paintComponent(Graphics g){
super.paintComponent(g);
this.setBackground(Color.BLACK);
g.setColor(Color.GREEN);
g.fillRect(150, 75, 200, 100);
g.setColor(Color.ORANGE);
g.drawString("Play", 239, 123);
njf.addMouseListener(new MouseAdapter() {
public void mouseMoved(MouseEvent e) {
x = e.getX();
y = e.getY();
}
});
if(x > 100 && y > 100){
g.drawRect(10, 10, 100, 100);
}
}
public GameSetup(){
njf.setSize(500,500);
njf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
njf.setResizable(false);
njf.setLocationRelativeTo(null);
njf.setVisible(true);
}
#Override
public void mouseDragged(MouseEvent arg0) {
}
#Override
public void mouseMoved(MouseEvent e) {
}
}
Okay, there are several things wrong with the code that you included above.
The first that stood out to me was the way that you are adding the mouse action listener to frame. There are multiple things that are wrong with this.
First of all, you are doing this in the paintComponent method, which, if it works, is still considered to be bad practice because the paintComponent method may be called multiple times. Do that, as pointed out by the comments, in the constructor of the panel.
The second is that you are adding the mouse listener to the frame, not the panel, which doesn't work because the panel is "above" the frame, so the mouse event will only be recognized within the panel. Your best bet here is to add a MouseMotionListener directly to the panel itself.
The third is that you are implementing the MouseMotionListenerinterface in the GameSetup class, but never actually doing anything with this implementation. So what I did was that I got rid of the inner class, and just had the panel be its own MouseMotionListnener
The second thing that is wrong with the code is that the paintComponent method is only called at certain points in time (see this). That means that even though the mouse might have moved within the zone, your paintComponent method isn't being called to update the screen accordingly. For this, you need to call the repaint method for your panel.
The third is that you didn't set a size for your panel, and the default one is 0x0, so you need to set a size of your panel (which should be the same as the frame itself) (if you want to keep the default layout).
All of that being said, here is your code that I fixed up. I added a variable called enteredZone to keep track of if the mouse had previously entered the zone, so that the rectangle would stay up even if the mouse left the zone after entering it (its your choice if you want to keep it). Note that there are other things with this code that might be considered bad practice, but this is enough to get you started:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
#SuppressWarnings("serial")
public class GameSetup extends JPanel implements MouseMotionListener {
public static JFrame njf = new JFrame("Test");
public static int x = 0, y = 0;
public static boolean enteredZone = false;
public static void main(String[] args) {
GameSetup gs = new GameSetup();
gs.addMouseMotionListener(gs);
njf.add(gs);
njf.setVisible(true);
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
this.setBackground(Color.BLACK);
g.setColor(Color.GREEN);
g.fillRect(150, 75, 200, 100);
g.setColor(Color.ORANGE);
g.drawString("Play", 239, 123);
if (x > 100 && y > 100 || enteredZone){
g.drawRect(10, 10, 100, 100);
enteredZone = true;
}
}
public GameSetup() {
super();
setSize(500, 500);
njf.setSize(500,500);
njf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
njf.setResizable(false);
njf.setLocationRelativeTo(null);
}
#Override
public void mouseDragged(MouseEvent arg0) {
}
#Override
public void mouseMoved(MouseEvent e) {
x = e.getX();
y = e.getY();
if (x > 100 && y > 100) repaint();
}
}
I was suggested not to use sleep for pausing purpose and instead use swing timer but still its not working.The animation i want to achieve is a ball travelling from the top left corner diagonally towards the bottom.
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
class Show_starter {
int x, y;
JFrame window = new JFrame("Graphic_show");
Graphic_panel jp = new Graphic_panel();
public static void main(String[] args) {
Show_starter start = new Show_starter();
start.go();
}
private void go() {
window.getContentPane().add(BorderLayout.CENTER, jp);
window.setSize(600,800);
window.setVisible(true);
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
class Graphic_panel extends JPanel {
public void paintComponent(Graphics g) {
for ( int i = 0; i < 100; ++i) {
g.setColor(Color.white);
g.fillRect(0, 0, this.getWidth(), this.getHeight());
g.setColor(Color.green);
g.fillOval(x, y, 40, 40);
x++;
y++;
try {
Timer tmr = new Timer(1000, new TimerListener()); //This fires an event after every 1 sec after it has started by start().
//But The ball travels too much fast and stops at a point and again travels very fast.
tmr.serRepeats(false); // even this is not working.
tmr.start();
//should i use repaint here or in Listener for this timer?
} catch (Exception e){}
}
}
class TimerListener implements ActionListener {
public void actionPerformed(ActionEvent e) {
jp.repaint();
}
}
}
}
The behaviour it is showing is very odd ball first goes at very high speed and stops momentarily at a point and again moves at same speed.
I have also tried to change the time for timer event to fire but the same thing happens.
Even inside loop where I have started timer the follow function call is not working
setRepeats(false);
don't create new timers always one timer is enough .decrees timing interval bit more .1 second is too slow for a animation.you don't need a loop and don't increment x and y inside paintcomponet() method because this method get called for some reason for example when you resize,minimize,maximize.
the unexpected behavior is due to your loop and creating new timers(). for example in x y start at zero but in first second x y increased to 100,100 in next paint you see them in a 100,100 position .that's look like quick move and then stop ...
example code (edited)
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
public class Show_starter {
private Timer tmr;
int x, y;
JFrame window = new JFrame("Graphic_show");
Graphic_panel jp = new Graphic_panel();
public static void main(String[] args) {
Show_starter start = new Show_starter();
start.go();
}
private void go() {
window.getContentPane().add(BorderLayout.CENTER, jp);
window.setSize(600, 800);
window.setVisible(true);
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
tmr = new Timer(10, new ActionListener() { // time gap in millisecond
#Override
public void actionPerformed(ActionEvent ae) {
jp.increse();
jp.repaint();
}
});
tmr.start();
}
class Graphic_panel extends JPanel {
public void increse() {
x++;
y++;
if (x > 100) { // stop animation at x>100
tmr.stop();
}
}
public void paintComponent(Graphics g) {
g.setColor(Color.white);
g.fillRect(0, 0, this.getWidth(), this.getHeight());
g.setColor(Color.green);
g.fillOval(x, y, 40, 40);
}
}
}
output (smoother than that)
Firstly you should not use a loop inside paint component.
public void paintComponent(Graphics g){
g.setColor(Color.white);
g.fillRect(0, 0, this.getWidth(), this.getHeight());
g.setColor(Color.green);
g.fillOval(x, y, 40, 40);
}
Just draw it and make x and y public in the class so that you can easily access from outside
Now run a runnable (consult invokelater and swingUtilities) or create a new Thread
inside public void run() do whatever animations you want to do . You can use a loop but add Thread.sleep(//in miliseconds) and repaint() in each iteration .
note : you need to change the x and y and repaint() so that it is drawn in a new position. And the sleep() is needed to slow down the execution otherwise it can't be visible to viewer.
There's no errors, but when I press any of the buttons, my oval/circle doesn't move at all? Can anyone help? I've been looking up and down the code for about 20 minutes seeing if I typed anything wrong or put something in the wrong place. I can't tell if this is with the way I'm moving it or my thread.
package com.badfitz66.mainpackage;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import javax.swing.JFrame;
public class Main extends JFrame implements Runnable
{
int x, y, xDirection, yDirection;
private Image dbImage;
private Graphics dbG;
Font font = new Font("Black Caps", Font.ITALIC | Font.BOLD, 30);
public void run()
{
try
{
while(true)
{
Move();
Thread.sleep(5);
}
}
catch(Exception e){
System.out.println("Error");
}
}
public void Move()
{
x += xDirection;
y += yDirection;
if (x <= 0)
x = 0;
if(x >= 500)
x = 500;
if (y <= 50)
y = 50;
if (y >= 250)
y = 250;
}
public void setXDir(int xdir)
{
xDirection = xdir;
}
public void setYDir(int ydir)
{
yDirection = ydir;
}
public class AL extends KeyAdapter
{
public void keyPressed(KeyEvent e)
{
int keyCode = e.getKeyCode();
if(keyCode == e.VK_D)
{
setXDir(+1);
}
if(keyCode == e.VK_A)
{
setXDir(-1);
}
if(keyCode == e.VK_W)
{
setYDir(-1);
}
if(keyCode == e.VK_S)
{
setYDir(+1);
}
}
public void keyReleased(KeyEvent e)
{
int keyCode = e.getKeyCode();
if(keyCode == e.VK_D)
{
setXDir(0);
}
if(keyCode == e.VK_A)
{
setXDir(0);
}
if(keyCode == e.VK_W)
{
setYDir(0);
}
if(keyCode == e.VK_S)
{
setYDir(0);
}
}
}
public Main()
{
addKeyListener(new AL());
setTitle("Java game testing");
setResizable(false);
setVisible(true);
setSize(500, 500);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setBackground(Color.green);
x = 150;
y = 150;
}
public void paint(Graphics g)
{
dbImage = createImage(getWidth(),getHeight());
dbG = dbImage.getGraphics();
paintComponent(dbG);
g.drawImage(dbImage, 0, 0, this);
}
public void paintComponent(Graphics g)
{
g.setFont(font);
g.drawString("Hello world", 125, 50);
g.setColor(Color.cyan);
g.fillOval(x, y, 15, 15);
repaint();
}
public static void main(String[] args)
{
Main jg = new Main();
//Threads
Thread t1 = new Thread();
t1.start();
}
}
You never call repaint from within your Move method
Thread t1 = new Thread(); won't do much, as it will never call any runnable code, in fact it will start and terminate almost immediately, how ever...
Swing is not thread safe and you should never modify the UI or anything the UI relies on from outside the Event Dispatching Thread, this especially important, as a paint cycle could occur at any time. See Concurrency in Swing for more details
You override the paint method of a top level container (JFrame) and then break the paint chain...paint is complex series of method calls chained together to generate the final result, you should always call super.paint first, but as you probably know, JFrame is not double buffered. So instead, you should create another class that extends from JPanel and override it's paintComponent method to perform the actual painting (in fact, for the most part, it should pretty much replace the functionality that the current JFrame is doing)...Swing components are double buffered by default...
Calling repaint from within a paint method...this is bad news and this will immediately schedule another paint cycle, this becomes so fast that it consume all your CPU cycles till you computer stands still
Using KeyListener. KeyListener is notorious for having issues, in particular, it will only ever trigger a key event if the component it is registered to IS FOCUSABLE and HAS FOCUS. A JFrame is made up of the physical window, the JRootPane, which holds the content pane (and a few other components), all of which can get in the way of the frame actually getting focus. Instead, using the previously mentioned JPanel, use the key bindings API, which will allow to control the level of focus required for the key events to be triggered. See How to Use Key Bindings for more details
You should also have a look at...
Performing Custom Painting
Painting in AWT and Swing
How to use Swing Timers
In the code below, the arrow keys are used to draw a line as in etch-a-sketch (but you have to resize the window first to trigger the panel's focus request at the right time -- that's a question for a different time, perhaps). The graphics command g.drawLine() occurs in the keyPressed() function, repaint() is not called explicitly, but the image updates itself as the arrow keys are pressed. Why is that? All the documentation I have looked at only talks about the JPanel automatically repainting itself when it is resized or uncovered.
import java.awt.event.*;
import java.awt.*;
import javax.swing.*;
public class Etch extends JPanel implements KeyListener{
public int xPrev, yPrev, xNew, yNew, inc;
public Etch(int start){
xPrev = start;
yPrev = start;
xNew = start;
yNew = start;
inc = 10;
addKeyListener(this);
}
public void paintComponent(Graphics g){
super.paintComponent(g);
this.requestFocusInWindow();
}
public static void main(String[] args)
{
JFrame w = new JFrame("Keyboard");
w.setBounds(100, 100, 600, 600);
w.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Etch panel = new Etch(200);
panel.setFocusable(true);
panel.setBackground(Color.RED);
Container c = w.getContentPane();
c.add(panel);
w.setResizable(true);
w.setVisible(true);
}
public void keyPressed(KeyEvent e){
int code = e.getKeyCode();
if (code == KeyEvent.VK_UP){
xPrev = xNew;
yPrev = yNew;
yNew -= inc;
}
if (code == KeyEvent.VK_DOWN){
xPrev = xNew;
yPrev = yNew;
yNew += inc;
}
if (code == KeyEvent.VK_LEFT){
xPrev = xNew;
yPrev = yNew;
xNew -= inc;
}
if (code == KeyEvent.VK_RIGHT){
xPrev = xNew;
yPrev = yNew;
xNew += inc;
}
Graphics g = this.getGraphics();
g.setColor(Color.BLUE);
g.drawLine(xPrev, yPrev, xNew, yNew);
}
// Not used but required by the KeyListener interface
public void keyReleased (KeyEvent e) { }
public void keyTyped (KeyEvent e) { }
}
this.requestFocusInWindow(); is a bad idea within the paintComponent method. Painting should paint the current state and never change the state of the component
getGraphics is not how painting is done in Swing. Custom painting should be done from within the context of the paintComponent method. See Painting in AWT and Swing and Performing Custom Painting for more details
Consider using key bindings over KeyListener, as you can control the focus level required to trigger the key events. See How to Use Key Bindings for more details
Remember, you don't control the paint process in Swing, a paint cycle can be triggered by any number of events, most of which you don't actually control. Try working within the process instead of out of it.
Start by creating a List of java.awt.Point. Add each point to the List when a key event occurs. Use the paintComponent to iterate over the List and paint the lines between the Points...