Java Game Development: Graphics
Asked Answered
M

2

3

i'm new here. Hope you will be able to help.

Problem: Problem with displaying Animation on JFrame. Seems I miss/don't understand enough how Java's graphics works.

Global idea: Lets say I want make a game/movie/clip. For this I need this (not)simple animation get working.

An example for this question: I got class Screen, which has the screen stuff- Declaration of the JFrame, setting up its configuration(size, close operation, etc) and then creating the objects of class Box, to be showed on the frame. Check please this image/diagram of the classes(hope I wrote it the right way): ClassesDiagram

Now, class Box extends JPanel. I inherit from JPanel the method Paint() and override it, painting boxes.

In the Screen class, after I created the two Boxes, i .add() them to the JFrame. Next, starts a loop while(true), and updates the box's position every 200 milisec by making the thread sleep that amount. (in this case, just simple x++ or y++ depends on which box, box1 or box2).

Main Problem 1): The program runs, and shows the JFrame, but on the JFrame it shows only the last added object/component- Box. It doesn't show the other one. Why?

I found a topic, How to add multiple components to a JFrame?, and tried the tips the most voted post gave, by jjnguy Nov 15 '10 at 17:02. For some odd reason, not the first, nor the second tip worked for me.

Main Problem 2): As far as I understand, i need layout manager. Why do I need it, if I just want to paint() at specific X,Y on the frame?

Found other post(cant find it again)+Oracle's guidelines about layouts, suggested that I need consider using setLayout(null); I tried to do so, but then there's a problem once again.

Main Problem 3): The JFrame shows up, it shows only 1 box(the green one wont show up, whatever you will do. Don't know why) and when it DOES move- it gets erased from the other side. Here: Box Movement

Thank you in advance for any help, tips, and explanation! Hope the post is clear, organised, and nice looking.


public class Screen {

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

private JFrame window;

public Screen() {
    window = new JFrame("Multiply components panel");
    //window.setLayout(null);
    window.setSize(200, 200);

    window.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);

    Box b1 = new Box(10,10, "box1");
    //b1.setBounds(10, 10, 50, 50);

    Box b2 = new Box(100,100, "box2");
    //b2.setBounds(100, 100, 50, 50);


    window.add(b1);
    //window.add(b2);


    window.setVisible(true);

    while (true){

        b1.update();
        b2.update();

        try {
            Thread.sleep(200);
        } catch (Exception e) {
            // TODO: handle exception
        }
    }

}
}

public class Box extends JPanel{

int x, y, w, h;
private String name;

public Box(int x, int y, String name) {
    this.x = x;
    this.y = y;
    this.w = 100;
    this.h = 100;
    this.name=name;
}


public void paint(Graphics g){
    System.out.println(this.name.equalsIgnoreCase("box1"));
    if(this.name.equalsIgnoreCase("box1")){
        g.setColor(Color.BLACK);
        g.fillRect(x, y, w, h);
    }else{
        g.setColor(Color.GREEN);
        g.fillRect(x, y, w, h);
    }


}


public void update() {
    if(this.name.equalsIgnoreCase("box1"))
        x++;
    else
        y++;
    //this.setBounds(x, y, w, h);
    System.out.println("Current "+name+": X: "+x+", Y: "+y+", W: "+w+", H: "+h);
    repaint();
}

}
Mongolism answered 28/6, 2013 at 21:29 Comment(8)
Have 1 year old to feed, but, take a look at Concurrency in Swing for starters, then search just SO for Swing and animationSynaeresis
@DavidKroukamp Thank you for the useful link. I will check it out, and try look how you did stuff there. Still, I would like to understand what I am missing. Making animation of just 2 objects shouldn't be SO complicated. I want to understand the idea behind it.Mongolism
@Synaeresis Well you will have to live with it, Papa. Thank you for the link, will check it out. Not sure how much useful it will be through. Seems its about deep Thread working, which I'm not sure I need. At least now.Mongolism
@DanielBenshtein see my answer belowCyprinoid
@DanielBenshtein Animation is the illusion of change over time. You need to some way to periodically change the state of your animation and then update it. Because any blocking action in Swing (like infinite loops) will prevent the Event Dispatching Thread for processing paint requests (amongst other things) y need to perform this wait state off the EDT. Depending on the complexity of your animation, you could use a javax.swing.Timer or you will a thread for more complex animations, but the link will help you understand some of the caveats you need to beware ofSynaeresis
Note that the link which MadProgrammer gave is part of the Official Java Tutorials. If you poke around a bit, you should be able to find the one for Swing. I suggest you work through it and especially pay attention to the sections about event handling.Pinochle
@Synaeresis Thank you very much! Now, after you explained about the EDT & the infinite loop I've made, I more or less understood. I have now new material to work on, read and learn.Mongolism
Don't override paint() and update(). That is done when using AWT. For custom painting in Swing you just override the paintComponent() method.Keeling
C
3

Main Problem 1): The program runs, and shows the JFrame, but on the JFrame it shows only the last added object/component- Box. It doesn't show the other one. Why?

You do window.add(b1); window.add(b2);, by default JFrame has BorderLayout thus you are replacing the last added box when you do add(..).

Main Problem 2): As far as I understand, i need layout manager. Why do I need it, if I just want to paint() at specific X,Y on the frame?

If you are using JPanels as objects in the game than, this would be the only time to use setLayout(null) as we want full control over the JPanels placing.

Main Problem 3): The JFrame shows up, it shows only 1 box(the green one wont show up, whatever you will do. Don't know why) and when it DOES move- it gets erased from the other side. Here: Box Movement

because you you do this g.fillRect(x,y,w,h), it should be g.fillRect(0,0,w,h)

Other problems:

1) I think a major problem you have is here:

public class Box extends JPanel{

    ...

    public void paint(Graphics g){

       ...
    }

}

You should override paintComponent of JPanel, and remember to call super.paintComponent(g) as first call in overridden method.

3) you should override getPreferredSize of JPanel and return correct dimensions which match the image or contents of the JPanel

4) dont call setSize on JFrame use correct Layoutmanager and/or override getPreferredSize of necessary containers than simply call pack() on JFrame before setting it visible.

5) as said by @MadProgrammer have a read on Concurrency in Swing but basically all Swing components should be created/manipulated on EDT via SwingUtilities.inokeXXX block.

6) doing this is definetly bad:

while (true){

    b1.update();
    b2.update();

    try {
        Thread.sleep(200);
    } catch (Exception e) {
        // TODO: handle exception
    }
}

as you are not only creating a continuous loop but also calling Thread.sleep on the Thread which you created you GUI, thus is will seem to freeze. Have a look at How to use Swing Timers, also see this similar question.answer on the above topic.

Here is code with above fixes implemented:

enter image description here

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import javax.swing.AbstractAction;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

public class Screen {

    private JFrame window;

    public static void main(String[] args) {

        //creat UI on EDT
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new Screen();
            }
        });
    }

    public Screen() {
        window = new JFrame("Multiply components panel") {
            @Override
            public Dimension getPreferredSize() {
                return new Dimension(600, 400);
            }
        };

        window.setLayout(null);//only reason this is warrented is because its a gmae using JPanels as game objects thus we need full control over its placing
        window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//changed from DISPOSE to EXIT so Timers  will be exited too

        final Box b1 = new Box(10, 10, 50, 50, "box1");

        final Box b2 = new Box(100, 100, 50, 50, "box2");

        window.add(b1);
        window.add(b2);

        window.pack();
        window.setVisible(true);

        Timer timer = new Timer(200, new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                b1.update();
                b2.update();
            }
        });
        timer.setInitialDelay(0);
        timer.start();

    }
}

class Box extends JPanel {

    int x, y, w, h;
    private String name;

    public Box(int x, int y, int w, int h, String name) {
        this.x = x;
        this.y = y;
        this.w = w;
        this.h = h;
        this.name = name;
        setBounds(x, y, w, h);//let the Box class handle setting the bounds more elegant OOP
    }

    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        System.out.println(this.name.equalsIgnoreCase("box1"));
        if (this.name.equalsIgnoreCase("box1")) {
            g.setColor(Color.BLACK);
            g.fillRect(0, 0, w, h);
        } else {
            g.setColor(Color.GREEN);
            g.fillRect(0, 0, w, h);
        }
    }

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

    public void update() {
        if (this.name.equalsIgnoreCase("box1")) {
            x++;
        } else {
            y++;
        }
        this.setBounds(x, y, w, h);//set the new bounds so box may be updated
        System.out.println("Current " + name + ": X: " + x + ", Y: " + y + ", W: " + w + ", H: " + h);
        revalidate();
        repaint();
    }
}

Lastly have a look at my tutorial/code snippet Game Development Loop, Logic and Collision detection Java Swing 2D.

It has all you need to start a simple 2D game, like game loop, logic, pixel collision detection, animation ( ie swapping between multiple sprites to create animation of the sprite set), and more, the fundamental difference though is it uses objects as the game entities, i.e a class will hold all the needed information for an object to be drawn, IMO this is how games should be done, a things might get graphically intense and have many JPanels wondering the screen will definitely drop overall FPS

Cyprinoid answered 28/6, 2013 at 21:47 Comment(3)
Thank you for your answer. I will check it carefully, and try to learn and understand it. Will write if I'll have any questions. One thing through- I tried to add another Box, it appeared while the first disappeared. :\Mongolism
@DanielBenshtein please see my updated post I tried my best to answer all your questions, and got both your boxes to show up instead of just the 1Cyprinoid
Thank you very much for the patience, and the good and full answer! Now what's left is to check out the code and learn, as well as the links you and the others gave(but that's now my work ;) Thanks!Mongolism
B
1

I realize this is an old question, but it's been viewed over 1,000 times.

When creating a Swing GUI, it's a good idea to use the model / view / controller (MVC) pattern. This pattern allows us to separate our concerns and focus on one part of the GUI at a time. If there's a problem, then we have a good idea where in the code the problem lies.

Here's the GUI I created.

Multiple Components GUI

So, let's start with the model. Here's the Box class.

public class Box {
    
    private final Color color;
    
    private final Point boxMotion;
    
    private final Rectangle rectangle;
    
    private final String name;

    public Box(String name, Color color, Rectangle rectangle, 
            Point boxMotion) {
        this.name = name;
        this.color = color;
        this.rectangle = rectangle;
        this.boxMotion = boxMotion;
    }
    
    public void update(int width, int height) {
        this.rectangle.x += boxMotion.x;
        this.rectangle.y += boxMotion.y;
        int boxWidth = rectangle.x + rectangle.width;
        int boxHeight = rectangle.y + rectangle.height;
        
        if (rectangle.x < 0) {
            rectangle.x = -rectangle.x;
            boxMotion.x = -boxMotion.x;
        }
        
        if (boxWidth > width) {
            rectangle.x = width - rectangle.width - boxMotion.x;
            boxMotion.x = -boxMotion.x;
        }
        
        if (rectangle.y < 0) {
            rectangle.y = -rectangle.y;
            boxMotion.y = -boxMotion.y;
        }
        
        if (boxHeight > height) {
            rectangle.y = height - rectangle.height - boxMotion.y;
            boxMotion.y = -boxMotion.y;
        }
    }

    public Color getColor() {
        return color;
    }

    public Rectangle getRectangle() {
        return rectangle;
    }

    public String getName() {
        return name;
    }
    
}

The Box class is a plain Java class. It has the typical getter methods. There are no setter classes, as the constructor sets all of the internal fields.

We make use of a java.awt.Rectangle and a java.awt.Point to hold the size of the box and the direction of box motion, respectively. This keeps us from having to define six int fields.

By saving the Color and direction of motion with each box, we can have many boxes with different colors and different directions. The direction of motion is technically a delta X and delta Y instance, but I wasn't going to create a separate class just to make that clear. The Point class is good enough.

The update method was the hardest to get correct. The update method controls the motion of the box, and checks for the box hitting the edges of the drawing JPanel. This method is included in the Box class because it acts on each box. This method will be executed by the controller.

The MVC pattern does not imply that the model, view, and controller code reside in separate classes, although that's the ideal. The code can go where it makes the most sense, as long as the execution of the code is separated by model, view, and controller.

Next, we'll look at the Boxes class.

public class Boxes {
    
    private final int drawingPanelWidth;
    private final int drawingPanelHeight;
    
    private final List<Box> boxes;
    
    public Boxes() {
        this.drawingPanelWidth = 600;
        this.drawingPanelHeight = 400;
        this.boxes = new ArrayList<>();
        addBoxesFactory();
    }
    
    private void addBoxesFactory() {
        Rectangle rectangle = new Rectangle(10, 10, 50, 50);
        Point point = new Point(3, 3);
        this.boxes.add(new Box("box1", Color.BLACK, rectangle, point));
        
        rectangle = new Rectangle(100, 100, 50, 50);
        point = new Point(-3, -3);
        this.boxes.add(new Box("box2", Color.GREEN, rectangle, point));
    }

    public int getDrawingPanelWidth() {
        return drawingPanelWidth;
    }

    public int getDrawingPanelHeight() {
        return drawingPanelHeight;
    }

    public List<Box> getBoxes() {
        return boxes;
    }
    
}

The Boxes class is a plain Java class. It has the typical getter methods. There are no setter classes, as the constructor sets all of the internal fields.

We use a List to hold as many Box instances as we want to display. In this class, we define two Box instances. We can define as many Box instances as we want. The rest of the code will process as many boxes as we define.

We define the width and height of the drawing JPanel in this class. We do this because the drawing JPanel will need those dimensions to size itself and because the controller will need those dimensions to determine when a Box instance hits the width or height edge.

Next, we'll look at the main view class, the BoxMotionGUI class.

public class BoxMotionGUI implements Runnable {

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new BoxMotionGUI());
    }
    
    private Boxes boxes;
    
    private DrawingPanel drawingPanel;
    
    public BoxMotionGUI() {
        this.boxes = new Boxes();
    }

    @Override
    public void run() {
        JFrame frame = new JFrame("Multiple Components GUI");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        
        drawingPanel = new DrawingPanel(boxes);
        frame.add(drawingPanel, BorderLayout.CENTER);
        
        frame.pack();
        frame.setLocationByPlatform(true);
        frame.setVisible(true);
        
        Timer timer = new Timer(20, new AbstractAction() {

            private static final long serialVersionUID = 1L;

            @Override
            public void actionPerformed(ActionEvent e) {
                for (Box box : boxes.getBoxes()) {
                    box.update(boxes.getDrawingPanelWidth(), 
                            boxes.getDrawingPanelHeight());
                    drawingPanel.repaint();
                }
            }
        });
        
        timer.setInitialDelay(0);
        timer.start();
    }

}

The BoxMotionGUI class starts with a call to the SwingUtilities invokeLater method. This method ensures that the Swing application will create the Swing components on the Event Dispatch Thread. A well-written Swing program uses concurrency to create a user interface that never "freezes".

The constructor creates one instance of the Boxes class. Generally, you create the model and pass the model to the view. The view reads from the model but does not update the model in any way. The controller classes will update the model and update / repaint the view.

The run method holds all of the code to create the JFrame. These methods must be called in this order. This order is the order I use for most of my Swing applications.

The java.swing.Timer code makes up the controller. I put this code in the view method because it's short. If the actionPerformed code was more involved, a separate controller class would be warranted.

The actionPerformed method updates the model and repaints the view.

Next, we'll look at the DrawingPanel class.

public class DrawingPanel extends JPanel {

    private static final long serialVersionUID = 1L;
    
    private final Boxes boxes;
    
    public DrawingPanel(Boxes boxes) {
        this.boxes = boxes;
        this.setPreferredSize(new Dimension(
                boxes.getDrawingPanelWidth(), 
                boxes.getDrawingPanelHeight()));
    }
    
    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        
        Graphics2D g2d = (Graphics2D) g;
        for (Box box : boxes.getBoxes()) {
            g2d.setColor(box.getColor());
            Rectangle rectangle = box.getRectangle();
            g2d.fillRect(rectangle.x, rectangle.y, 
                    rectangle.width, rectangle.height);
        }
    }
    
}

The DrawingPanel class draws the Box instances. We extend a JPanel because we want to override the paintComponent method. The only reason you should ever extend a Swing component, or any Java class, is if you want to override one or more of the class methods.

The paintComponent method paints. It doesn't try and do anything else. Since this method gets called every time the GUI is repainted, you want to do the minimum amount of processing possible. All other processing besides painting goes somewhere else.

Finally, here's the complete runnable code.

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.List;

import javax.swing.AbstractAction;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

public class BoxMotionGUI implements Runnable {

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new BoxMotionGUI());
    }
    
    private Boxes boxes;
    
    private DrawingPanel drawingPanel;
    
    public BoxMotionGUI() {
        this.boxes = new Boxes();
    }

    @Override
    public void run() {
        JFrame frame = new JFrame("Multiple Components GUI");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        
        drawingPanel = new DrawingPanel(boxes);
        frame.add(drawingPanel, BorderLayout.CENTER);
        
        frame.pack();
        frame.setLocationByPlatform(true);
        frame.setVisible(true);
        
        Timer timer = new Timer(20, new AbstractAction() {

            private static final long serialVersionUID = 1L;

            @Override
            public void actionPerformed(ActionEvent e) {
                for (Box box : boxes.getBoxes()) {
                    box.update(boxes.getDrawingPanelWidth(), 
                            boxes.getDrawingPanelHeight());
                    drawingPanel.repaint();
                }
            }
        });
        
        timer.setInitialDelay(0);
        timer.start();
    }
    
    public class DrawingPanel extends JPanel {

        private static final long serialVersionUID = 1L;
        
        private final Boxes boxes;
        
        public DrawingPanel(Boxes boxes) {
            this.boxes = boxes;
            this.setPreferredSize(new Dimension(
                    boxes.getDrawingPanelWidth(), 
                    boxes.getDrawingPanelHeight()));
        }
        
        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            
            Graphics2D g2d = (Graphics2D) g;
            for (Box box : boxes.getBoxes()) {
                g2d.setColor(box.getColor());
                Rectangle rectangle = box.getRectangle();
                g2d.fillRect(rectangle.x, rectangle.y, 
                        rectangle.width, rectangle.height);
            }
        }
        
    }
    
    public class Boxes {
        
        private final int drawingPanelWidth;
        private final int drawingPanelHeight;
        
        private final List<Box> boxes;
        
        public Boxes() {
            this.drawingPanelWidth = 600;
            this.drawingPanelHeight = 400;
            this.boxes = new ArrayList<>();
            addBoxesFactory();
        }
        
        private void addBoxesFactory() {
            Rectangle rectangle = new Rectangle(10, 10, 50, 50);
            Point point = new Point(3, 3);
            this.boxes.add(new Box("box1", Color.BLACK, rectangle, point));
            
            rectangle = new Rectangle(100, 100, 50, 50);
            point = new Point(-3, -3);
            this.boxes.add(new Box("box2", Color.GREEN, rectangle, point));
        }

        public int getDrawingPanelWidth() {
            return drawingPanelWidth;
        }

        public int getDrawingPanelHeight() {
            return drawingPanelHeight;
        }

        public List<Box> getBoxes() {
            return boxes;
        }
        
    }
    
    public class Box {
        
        private final Color color;
        
        private final Point boxMotion;
        
        private final Rectangle rectangle;
        
        private final String name;

        public Box(String name, Color color, Rectangle rectangle, 
                Point boxMotion) {
            this.name = name;
            this.color = color;
            this.rectangle = rectangle;
            this.boxMotion = boxMotion;
        }
        
        public void update(int width, int height) {
            this.rectangle.x += boxMotion.x;
            this.rectangle.y += boxMotion.y;
            int boxWidth = rectangle.x + rectangle.width;
            int boxHeight = rectangle.y + rectangle.height;
            
            if (rectangle.x < 0) {
                rectangle.x = -rectangle.x;
                boxMotion.x = -boxMotion.x;
            }
            
            if (boxWidth > width) {
                rectangle.x = width - rectangle.width - boxMotion.x;
                boxMotion.x = -boxMotion.x;
            }
            
            if (rectangle.y < 0) {
                rectangle.y = -rectangle.y;
                boxMotion.y = -boxMotion.y;
            }
            
            if (boxHeight > height) {
                rectangle.y = height - rectangle.height - boxMotion.y;
                boxMotion.y = -boxMotion.y;
            }
        }

        public Color getColor() {
            return color;
        }

        public Rectangle getRectangle() {
            return rectangle;
        }

        public String getName() {
            return name;
        }
        
    }

}
Biancabiancha answered 22/12, 2020 at 12:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.