Game Development Loop, Logic and Collision detection Java Swing 2D

DavidKroukamp 3 Tallied Votes 2K Views Share

I have recently been interested in Java Swing and Game development, so naturally I began creating many different 2D games.

During them I found myself having to rewrite much code, but eventually I decided to write some classes that would help me whenever I wanted to make a game.

Here is the basics for a single threaded game loop with multiple user input (i.e player moves with W, S, A and D and player 2 with UP, LEFT, RIGHT, and DOWN. Player1 can shoot with SPACE and player 2 wuth ENTER)

Basically there is an abstract class GameLoop which require you too override draw(), update(float elapsedTime) and checkCollisions(). This GameLoop will that be started and will call those methods as to render the screen at a set amount of frames per second. It uses a Fixed Time Step Game loop and is startable, pauseable/resumable and stopable. See here for more.

Next we have an Animator class which basicaly allows us to add an ArrayList of BufferedImages and timings for the images to be displayed. This class is mainly there to be used with GameObject. If you were wondering wwhy GameLoop had a method update(float elapsedTime) it is for the Animator class so it know how much time has passed and wether or not it should change images currently displayed.

GameObject class as the name suggest represents any object that we can draw to the GamePanel. It extedns the Animator class which allows it the functionality to have multiple images/sprites added to the Gamebject and displayed. It featires some advanced things like the direction the sprite is currently facing (i.e a person face from side view has to go left or right and image must be visually facing forward in that direction in this case it has to do with which side the bullets go depeding on which direction GameObject is going)

Bullet simply an extension of GameObject and is used so I could have a more meaningful game :)

Next is ImageScaler this was added for some over the top functionality IMO. It allows us to define a screen size that is standrad for our sprites i.e sprite is 100x100 when GUI is 800x600. Now usually if we changed the screens size for our app we would have to go an change all the picture etc, this method makes it easir for us to scale the images to new sizes in resepect of the current given frame/gui size.

KeyBinding is a class which is just a wrapper for the methods needed to add KeyBindings to Swing components.

GamePanel is the class which extends JPanel and in which all of the above come together, It holds an ArrayList with method to allow addition of GameObjects etc, tese are than drawn in paintComponent(..) it does have some extras too like anti-aliasing etc. Here is where we see some simple collsions detection taking place checking wether the Rectangle2D of the GameObjects intersects etc createAndShowGui() method also holds some important code which sets up everything including GamePanel.

I hope this will help others out there when creating their own games (even if if just the ideas)

gametest

import java.awt.BorderLayout;
    import java.awt.Color;
    import java.awt.Dimension;
    import java.awt.Graphics;
    import java.awt.Graphics2D;
    import java.awt.RenderingHints;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.awt.event.KeyEvent;
    import java.awt.geom.Rectangle2D;
    import java.awt.image.BufferedImage;
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    import java.util.concurrent.atomic.AtomicBoolean;
    import javax.swing.AbstractAction;
    import javax.swing.JButton;
    import javax.swing.JComponent;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    import javax.swing.KeyStroke;
    import javax.swing.SwingUtilities;
    import javax.swing.UIManager;
    import javax.swing.UIManager.LookAndFeelInfo;
    import javax.swing.UnsupportedLookAndFeelException;
    
    public class GameTest {
    
        public static final int HERTZ = 60;
        public static final int WIDTH = 800, HEIGHT = 600;
        public static final Dimension STANDARD_IMAGE_SCREEN_SIZE = new Dimension(800, 600);
        private ImageScaler is = new ImageScaler(STANDARD_IMAGE_SCREEN_SIZE, new Dimension(WIDTH, HEIGHT));//create instance to allow creating of image sizes for current screen size and width
    
        public GameTest() {
            createAndShowGui();
        }
    
        //code starts here
        public static void main(String[] args) {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    try {
                        for (LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
                            if ("Nimbus".equals(info.getName())) {
                                UIManager.setLookAndFeel(info.getClassName());
                                break;
                            }
                        }
                    } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException e) {
                        // If Nimbus is not available, you can set the GUI to another look and feel.
                    }
    
                    new GameTest();//create an instance which incudes GUI etc
                }
            });
        }
    
        private void createAndShowGui() {
            JFrame frame = new JFrame(GameTest.class.getSimpleName());
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setResizable(false);
    
            final GamePanel gamePanel = new GamePanel(HERTZ, WIDTH, HEIGHT);
    
            JPanel buttonPanel = new JPanel();
    
            //create buttons to control game loop start pause/resume and stop
            final JButton startButton = new JButton("Start");
    
            final JButton pauseButton = new JButton("Pause");
            pauseButton.setEnabled(false);
    
            final JButton stopButton = new JButton("Stop");
            stopButton.setEnabled(false);
    
            //add listeners to buttons 
            startButton.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent ae) {
                    //clear enitites currently in array
                    gamePanel.clearGameObjects();
                    //create test images for game object
                    ArrayList<BufferedImage> images = new ArrayList<>();
                    ArrayList<Long> timings = new ArrayList<>();
                    images.add(createColouredImage(Color.RED, 100, 100, false));
                    timings.add(300l);
                    images.add(createColouredImage(Color.BLACK, 100, 100, false));
                    timings.add(300l);
                    images.add(createColouredImage(Color.GREEN, 100, 100, false));
                    timings.add(300l);
    
                    //create arraylist of images scaled for the current screen size
                    ArrayList<BufferedImage> scaledImages = is.scaleImages(images);
    
                    //get starting position according to current screen size
                    //the position 0f 200,300 is on standrad screen size of 800,600
                    int startingXPlayer1 = (int) (200 * is.getWidthScaleFactor());
                    int startingYPlayer1 = (int) (300 * is.getHeightScaleFactor());
    
                    //create player 1 game onject which can be controlled by W,S,A,D and SPACE to shoot
                    final GameObject player1GameObject = new GameObject(startingXPlayer1, startingYPlayer1, scaledImages, timings, GameObject.RIGHT_FACING, gamePanel.getWidth(), gamePanel.getHeight());
    
                    images.clear();//clear arrays so we can add images for another just saving time
                    timings.clear();
    
                    images.add(createColouredImage(Color.CYAN, 100, 100, false));
                    timings.add(500l);//show cyan for 500 milis
                    images.add(createColouredImage(Color.YELLOW, 100, 100, false));
                    timings.add(1500l);//show yellow for 500 milis
    
                    //create arraylist of images scaled for the current screen size
                    scaledImages = is.scaleImages(images);
    
                    //get starting position according to current screen size
                    //the position 0f 400,100 is on standrad screen size of 800,600
                    int startingXPlayer2 = (int) (400 * is.getWidthScaleFactor());
                    int startingYPlayer2 = (int) (100 * is.getHeightScaleFactor());
    
                    final GameObject player2GameObject = new GameObject(startingXPlayer2, startingYPlayer2, scaledImages, timings, GameObject.LEFT_FACING, gamePanel.getWidth(), gamePanel.getHeight());
    
                    //add gameobjetcs to the gamepanel
                    gamePanel.addGameObject(player1GameObject);
                    gamePanel.addGameObject(player2GameObject);
    
                    GameKeyBindings gameKeyBindings = new GameKeyBindings(gamePanel, player1GameObject, player2GameObject);
    
                    gamePanel.start();
    
                    startButton.setEnabled(false);
                    pauseButton.setEnabled(true);
                    stopButton.setEnabled(true);
                }
            });
    
            pauseButton.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent ae) {
                    //checks if the game is paused or not and reacts by either resuming or pausing the game
                    if (gamePanel.isPaused()) {
                        gamePanel.resume();
                    } else {
                        gamePanel.pause();
                    }
                    if (pauseButton.getText().equals("Pause")) {
                        pauseButton.setText("Resume");
                    } else {
                        pauseButton.setText("Pause");
                        gamePanel.requestFocusInWindow();//button might have focus
                    }
                }
            });
    
            stopButton.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent ae) {
                    gamePanel.stop();
                    /* 
                     //if we want enitites to be cleared and a blank panel shown
                     gp.clearGameObjects();
                     gp.repaint(); 
                     */
                    if (!pauseButton.getText().equals("Pause")) {
                        pauseButton.setText("Pause");
                    }
                    startButton.setEnabled(true);
                    pauseButton.setEnabled(false);
                    stopButton.setEnabled(false);
                }
            });
    
    
            //add buttons to panel
            buttonPanel.add(startButton);
            buttonPanel.add(pauseButton);
            buttonPanel.add(stopButton);
            //add game panel and button panel to jframe
            frame.add(gamePanel, BorderLayout.CENTER);
            frame.add(buttonPanel, BorderLayout.SOUTH);
    
            frame.pack();
            frame.setVisible(true);
        }
    
        public static BufferedImage createColouredImage(Color color, int w, int h, boolean circular) {
            BufferedImage img = new BufferedImage(w, h, BufferedImage.TRANSLUCENT);
            Graphics2D g2 = img.createGraphics();
            GamePanel.applyRenderHints(g2);
            g2.setColor(color);
            if (!circular) {
                g2.fillRect(0, 0, img.getWidth(), img.getHeight());
            } else {
                g2.fillOval(0, 0, w, h);
            }
            g2.dispose();
            return img;
        }
    }
    
    class GameKeyBindings {
    
        public GameKeyBindings(final GamePanel gp, final GameObject player1GameObject) {
    
            KeyBinding.putKeyBindingOnPressAndRelease(gp, KeyBinding.WHEN_IN_FOCUSED_WINDOW, KeyEvent.VK_D, new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent ae) {
                    player1GameObject.RIGHT = true;
                }
            }, "D pressed", new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent ae) {
                    player1GameObject.RIGHT = false;
                }
            }, "D released");
    
            KeyBinding.putKeyBindingOnPressAndRelease(gp, KeyBinding.WHEN_IN_FOCUSED_WINDOW, KeyEvent.VK_A, new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent ae) {
                    player1GameObject.LEFT = true;
                }
            }, "A pressed", new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent ae) {
                    player1GameObject.LEFT = false;
                }
            }, "A released");
    
            KeyBinding.putKeyBindingOnPressAndRelease(gp, KeyBinding.WHEN_IN_FOCUSED_WINDOW, KeyEvent.VK_W, new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent ae) {
                    player1GameObject.UP = true;
                }
            }, "W pressed", new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent ae) {
                    player1GameObject.UP = false;
                }
            }, "W released");
            KeyBinding.putKeyBindingOnPressAndRelease(gp, KeyBinding.WHEN_IN_FOCUSED_WINDOW, KeyEvent.VK_S, new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent ae) {
                    player1GameObject.DOWN = true;
                }
            }, "S pressed", new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent ae) {
                    player1GameObject.DOWN = false;
                }
            }, "S released");
    
            final ArrayList<BufferedImage> images = new ArrayList<>();
            final ArrayList<Long> timings = new ArrayList<>();
            images.add(GameTest.createColouredImage(Color.darkGray.brighter(), 10, 10, true));
            timings.add(300l);
            KeyBinding.putKeyBindingOnPress(gp, KeyBinding.WHEN_IN_FOCUSED_WINDOW, KeyEvent.VK_SPACE, new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent ae) {
                    Bullet bullet = new Bullet((int) (player1GameObject.getX() + player1GameObject.getHeight() / 2), (int) (player1GameObject.getY() + player1GameObject.getWidth() / 2), images, timings, GameObject.RIGHT_FACING, gp.getWidth(), gp.getHeight(), player1GameObject, gp);
    
                    gp.addGameObject(bullet);//add the bullet to the List of GameObjects that will be drawn on next screen paint
                }
            }, "Space pressed");
        }
    
        GameKeyBindings(final GamePanel gp, GameObject player1GameObject, final GameObject player2GameObject) {
            this(gp, player1GameObject);
    
            KeyBinding.putKeyBindingOnPressAndRelease(gp, KeyBinding.WHEN_IN_FOCUSED_WINDOW, KeyEvent.VK_RIGHT, new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent ae) {
                    player2GameObject.RIGHT = true;
                }
            }, "right pressed", new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent ae) {
                    player2GameObject.RIGHT = false;
                }
            }, "right released");
    
            KeyBinding.putKeyBindingOnPressAndRelease(gp, KeyBinding.WHEN_IN_FOCUSED_WINDOW, KeyEvent.VK_LEFT, new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent ae) {
                    player2GameObject.LEFT = true;
                }
            }, "left pressed", new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent ae) {
                    player2GameObject.LEFT = false;
                }
            }, "left released");
    
            KeyBinding.putKeyBindingOnPressAndRelease(gp, KeyBinding.WHEN_IN_FOCUSED_WINDOW, KeyEvent.VK_UP, new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent ae) {
                    player2GameObject.UP = true;
                }
            }, "up pressed", new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent ae) {
                    player2GameObject.UP = false;
                }
            }, "up released");
            KeyBinding.putKeyBindingOnPressAndRelease(gp, KeyBinding.WHEN_IN_FOCUSED_WINDOW, KeyEvent.VK_DOWN, new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent ae) {
                    player2GameObject.DOWN = true;
                }
            }, "down pressed", new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent ae) {
                    player2GameObject.DOWN = false;
                }
            }, "down released");
    
            final ArrayList<BufferedImage> images = new ArrayList<>();
            final ArrayList<Long> timings = new ArrayList<>();
            images.add(GameTest.createColouredImage(Color.MAGENTA, 10, 10, true));
            timings.add(0l);
    
    
            KeyBinding.putKeyBindingOnPress(gp, KeyBinding.WHEN_IN_FOCUSED_WINDOW, KeyEvent.VK_ENTER, new AbstractAction() {
                @Override
                public void actionPerformed(ActionEvent ae) {
                    Bullet bullet = new Bullet((int) (player2GameObject.getX() + player2GameObject.getHeight() / 2), (int) (player2GameObject.getY() + player2GameObject.getWidth() / 2), images, timings, GameObject.RIGHT_FACING, gp.getWidth(), gp.getHeight(), player2GameObject, gp);
    
                    gp.addGameObject(bullet);//add the bullet to the List of GameObjects that will be drawn on next screen paint
                }
            }, "Enter pressed");
        }
    }
    
    class KeyBinding {
    
        private final JComponent container;
        private final int inputMap;
        public static final int WHEN_IN_FOCUSED_WINDOW = JComponent.WHEN_IN_FOCUSED_WINDOW,
                WHEN_FOCUSED = JComponent.WHEN_FOCUSED,
                WHEN_ANCESTOR_OF_FOCUSED_COMPONENT = JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT;
    
        public KeyBinding(JComponent container, int inputMap) {
            this.container = container;
            this.inputMap = inputMap;
        }
    
        public void addKeyBinding(int key, boolean onRelease, AbstractAction keybindingAction, String description) {
            putKeyBinding(container, inputMap, key, onRelease, keybindingAction, description);
        }
    
        public void addKeyBindingOnPress(int key, AbstractAction keybindingAction, String description) {
            putKeyBinding(container, inputMap, key, false, keybindingAction, description);
        }
    
        public void addKeyBindingOnRelease(int key, AbstractAction keybindingAction, String description) {
            putKeyBinding(container, inputMap, key, true, keybindingAction, description);
        }
    
        public void addKeyBindingOnPressAndRelease(int key, AbstractAction onPressAction, String onPressDesc, AbstractAction onReleaseAction, String onReleaseDesc) {
            putKeyBinding(container, inputMap, key, false, onPressAction, onPressDesc);
            putKeyBinding(container, inputMap, key, true, onReleaseAction, onReleaseDesc);
        }
    
        public static void putKeyBinding(JComponent container, int inputMap, int key, boolean onRelease, AbstractAction keybindingAction, String description) {
            container.getInputMap(inputMap).put(KeyStroke.getKeyStroke(key, 0, onRelease), description);
            container.getActionMap().put(description, keybindingAction);
        }
    
        public static void putKeyBindingOnPress(JComponent container, int inputMap, int key, AbstractAction keybindingAction, String description) {
            container.getInputMap(inputMap).put(KeyStroke.getKeyStroke(key, 0, false), description);
            container.getActionMap().put(description, keybindingAction);
        }
    
        public static void putKeyBindingOnRelease(JComponent container, int inputMap, int key, AbstractAction keybindingAction, String description) {
            container.getInputMap(inputMap).put(KeyStroke.getKeyStroke(key, 0, true), description);
            container.getActionMap().put(description, keybindingAction);
        }
    
        public static void putKeyBindingOnPressAndRelease(JComponent container, int inputMap, int key, AbstractAction onPressAction, String onPressDesc, AbstractAction onReleaseAction, String onReleaseDesc) {
            putKeyBinding(container, inputMap, key, false, onPressAction, onPressDesc);
            putKeyBinding(container, inputMap, key, true, onReleaseAction, onReleaseDesc);
        }
    }
    
    class GamePanel extends JPanel {
    
        private final static RenderingHints textRenderHints = new RenderingHints(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
        private final static RenderingHints imageRenderHints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        private final static RenderingHints colorRenderHints = new RenderingHints(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
        private final static RenderingHints interpolationRenderHints = new RenderingHints(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
        private final static RenderingHints renderHints = new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
        private GameLoop gameLoop;
        private int width, height;
        protected final List<GameObject> gameObjects = Collections.synchronizedList(new ArrayList<GameObject>());
    
        public GamePanel(int fps, int w, int h) {
            super(true);
            width = w;
            height = h;
            setIgnoreRepaint(true);//we will do the repainting
    
            gameLoop = new GameLoop(fps) {
                @Override
                void update(long elapsedTime) {//updates GameObject movement and Animation
                    updateGameObjects(elapsedTime);
                }
    
                @Override
                void draw() {//repaints the screen
                    repaint();
                }
    
                @Override
                void checkCollisions() {//collisons are checked after draw() is called
                    //do GameObject collision checks here
                    ArrayList<GameObject> gameObs = new ArrayList<>(gameObjects);
                    for (GameObject go : gameObs) {//iterate through all game objects
                        if (go instanceof Bullet) {//check if its a bullet
                            //get Bullet instance and its owner
                            Bullet bullet = (Bullet) go;
                            GameObject bulletOwner = bullet.getOwner();
    
                            for (GameObject go2 : gameObs) {//go trhough each object again
                                if (!go2.equals(bullet)) {//make sure we are not checking the bullet against itself
                                    if (bullet.intersects(go2) && !go2.equals(bulletOwner)) {//check if the bullets intersects any other object besides its owner
                                        System.out.println("A bullet hit a GameObject other than its owner");
                                        //bullet has hit get rid of it
                                        bullet.setVisible(false);
                                        removeGameObject(bullet);
                                    }
                                }
                            }
                        } else {//its not a Nullte thus must be a GameObject
                            for (GameObject go2 : gameObs) {//go through each GameObject again
                                if (!go.equals(go2)) {//make sure its not checking itself
                                    if (!(go2 instanceof Bullet)) {//make sure we dont chec bulltes as this has been done above
                                        if (go.intersects(go2)) {//check if the 2 gameobjects are intersecting each other
                                            System.out.println("A GameObject intersected another");
                                        }
                                    }
                                }
                            }
                        }
                    }
    
                }
            };
        }
    
        public void addGameObject(GameObject go) {
            gameObjects.add(go);
        }
    
        public void removeGameObject(GameObject go) {
            gameObjects.remove(go);
        }
    
        public void clearGameObjects() {
            gameObjects.clear();
        }
    
        @Override
        public Dimension getPreferredSize() {
            return new Dimension(width, height);
        }
    
        private void updateGameObjects(long elapsedTime) {
            ArrayList<GameObject> gameObs = new ArrayList<>(gameObjects);
            for (GameObject gameObject : gameObs) {
                if (gameObject.isVisible()) {
                    //update the movement and image of the gameobject
                    gameObject.update(elapsedTime);//updates the images i.e if its more than a single imag it will check to see if enough time has passed to make a change
                    gameObject.move();
                }
            }
        }
    
        @Override
        protected void paintComponent(Graphics grphcs) {
            super.paintComponent(grphcs);
    
            Graphics2D g2d = (Graphics2D) grphcs;
    
            applyRenderHints(g2d);
    
            //draw background
            g2d.setColor(Color.WHITE);
            g2d.fillRect(0, 0, getWidth(), getHeight());
    
            //draw all GameObjects to the screen
            ArrayList<GameObject> gameObs = new ArrayList<>(gameObjects);
            for (GameObject gameObject : gameObs) {
                if (gameObject.isVisible()) {
                    //draw the object to JPanel
                    g2d.drawImage(gameObject.getCurrentImage(), (int) gameObject.getX(), (int) gameObject.getY(), null);
                }
            }
        }
    
        public void start() {
            gameLoop.start();
        }
    
        public void pause() {
            gameLoop.pause();
        }
    
        public void resume() {
            gameLoop.resume();
        }
    
        public void stop() {
            gameLoop.stop();
        }
    
        public boolean isPaused() {
            return gameLoop.isPaused();
        }
    
        public boolean isRunning() {
            return gameLoop.isRunning();
        }
    
        public static void applyRenderHints(Graphics2D g2d) {
            g2d.setRenderingHints(textRenderHints);
            g2d.setRenderingHints(imageRenderHints);
            g2d.setRenderingHints(colorRenderHints);
            g2d.setRenderingHints(interpolationRenderHints);
            g2d.setRenderingHints(renderHints);
        }
    }
    
    class Bullet extends GameObject {
    
        private final GameObject owner;
        private final GamePanel gp;
    
        public Bullet(int x, int y, ArrayList<BufferedImage> frames, ArrayList<Long> timings, int pos, int conW, int conH, final GameObject owner, GamePanel gp) {
            super(x, y, frames, timings, pos, conW, conH);
            this.owner = owner;
            //add the bullet to the List of GameObjects that will be drawn on next screen paint
            setSpeed(DEFAULT_SPEED * 2); // setSpeed(getSpeed() * 2);
            //check which way the game object is facing and make bullet go accrodingly
            if (owner.getPosition() == GameObject.LEFT_FACING) {
                LEFT = true;
            } else if (owner.getPosition() == GameObject.RIGHT_FACING) {
                RIGHT = true;
            }
            this.gp = gp;
        }
    
        public GameObject getOwner() {
            return owner;
        }
    
        @Override
        public void move() {
            if ((rectangle.x - speed) <= 0) {
                setVisible(false);
                gp.removeGameObject(this);
            } else if (LEFT) {
                position = LEFT_FACING;
                rectangle.x -= speed;
            }
            if ((rectangle.x + speed) + getWidth() >= conW) {
                setVisible(false);
                gp.removeGameObject(this);
            } else if (RIGHT) {
                position = RIGHT_FACING;
                rectangle.x += speed;
            }
            if ((rectangle.y - speed) <= 0) {
                setVisible(false);
                gp.removeGameObject(this);
            } else if (UP) {
                rectangle.y -= speed;
            }
            if ((rectangle.y + speed) + getHeight() >= conH) {
                setVisible(false);
                gp.removeGameObject(this);
            } else if (DOWN) {
                rectangle.y += speed;
            }
        }
    }
    
    class ImageScaler {
    
        private final Dimension origScreenSize;
        private final Dimension newScreenSize;
    
        public ImageScaler(Dimension origScreenSize, Dimension newScreenSize) {
            this.origScreenSize = origScreenSize;
            this.newScreenSize = newScreenSize;
        }
    
        public double getWidthScaleFactor() {
            return (double) newScreenSize.width / origScreenSize.width;
        }
    
        public double getHeightScaleFactor() {
            return (double) newScreenSize.height / origScreenSize.height;
        }
    
        public BufferedImage scaleImage(BufferedImage img) {
            int width = (int) (img.getWidth() * getWidthScaleFactor());
            int height = (int) (img.getHeight() * getHeightScaleFactor());
            return scaleImage(img, width, height);
        }
    
        public ArrayList<BufferedImage> scaleImages(ArrayList<BufferedImage> images) {
            ArrayList<BufferedImage> imgs = new ArrayList<>();
            for (BufferedImage bImg : images) {
                int width = (int) (bImg.getWidth() * getWidthScaleFactor());
                int height = (int) (bImg.getHeight() * getHeightScaleFactor());
                imgs.add(ImageScaler.scaleImage(bImg, width, height));
            }
            return imgs;
        }
    
        public static BufferedImage scaleImage(BufferedImage bimg, int width, int height) {
            BufferedImage bi;
            try {
                bi = new BufferedImage(width, height, BufferedImage.TRANSLUCENT);
                Graphics2D g2d = (Graphics2D) bi.createGraphics();
                GamePanel.applyRenderHints(g2d);
                g2d.drawImage(bimg, 0, 0, width, height, null);
            } catch (Exception e) {
                return null;
            }
            return bi;
        }
    }
    
    class GameObject extends Animator {
    
        protected Rectangle2D.Double rectangle;
        protected int speed, position;
        private boolean visible;
        public boolean LEFT, RIGHT, UP, DOWN;
        //used to keep track of the actual image direction 
        public final static int LEFT_FACING = -1, RIGHT_FACING = 1, DEFAULT_SPEED = 5;
        protected final int conW, conH;
    
        public GameObject(int x, int y, ArrayList<BufferedImage> frames, ArrayList<Long> timings, int pos, int conW, int conH) {
            super(frames, timings);
            UP = false;
            DOWN = false;
            LEFT = false;
            RIGHT = false;
            visible = true;
            speed = DEFAULT_SPEED;
            position = pos;
            rectangle = new Rectangle2D.Double(x, y, getCurrentImage().getWidth(), getCurrentImage().getHeight());
            this.conW = conW;
            this.conH = conH;
        }
    
        @Override
        public void update(long elapsedTime) {
            super.update(elapsedTime);
            getWidth();//set the rectangles height accordingly after image update
            getHeight();//set rectangles height accordingle after update
        }
    
        public void setX(double x) {
            rectangle.x = x;
        }
    
        public void setY(double y) {
            rectangle.y = y;
        }
    
        public void setWidth(double width) {
            rectangle.width = width;
        }
    
        public void setHeight(double height) {
            rectangle.height = height;
        }
    
        public double getX() {
            return rectangle.x;
        }
    
        public double getY() {
            return rectangle.y;
        }
    
        public double getWidth() {
            if (getCurrentImage() == null) {//there might be no image (which is unwanted ofcourse but  we must not get NPE so we check for null and return 0
                return rectangle.width = 0;
            }
    
            return rectangle.width = getCurrentImage().getWidth();
        }
    
        public double getHeight() {
            if (getCurrentImage() == null) {
                return rectangle.height = 0;
            }
            return rectangle.height = getCurrentImage().getHeight();
        }
    
        public void setSpeed(int s) {
            speed = s;
        }
    
        public int getSpeed() {
            return speed;
        }
    
        public boolean isVisible() {
            return visible;
        }
    
        public void setVisible(boolean v) {
            visible = v;
        }
    
        public void move() {
            if (LEFT && (rectangle.x - speed) >= 0) {
                position = LEFT_FACING;
                rectangle.x -= speed;
            }
            if (RIGHT && (rectangle.x + speed) + getWidth() <= conW) {
                position = RIGHT_FACING;
                rectangle.x += speed;
            }
            if (UP && (rectangle.y - speed) >= 0) {
                rectangle.y -= speed;
            }
            if (DOWN && (rectangle.y + speed) + getHeight() <= conH) {
                rectangle.y += speed;
            }
        }
    
        public int getPosition() {
            return position;
        }
    
        public Rectangle2D getBounds2D() {
            return rectangle.getBounds2D();
        }
    
        public boolean intersects(GameObject go) {
            return rectangle.intersects(go.getBounds2D());
        }
    }
    
    class Animator {
    
        private ArrayList<BufferedImage> frames;
        private ArrayList<Long> timings;
        private int currIndex;
        private long animationTime;
        private long totalAnimationDuration;
        private AtomicBoolean done;//used to keep track if a single set of frames/ an animtion has finished its loop
    
        public Animator(ArrayList<BufferedImage> frames, ArrayList< Long> timings) {
            currIndex = 0;
            animationTime = 0;
            totalAnimationDuration = 0;
            done = new AtomicBoolean(false);
            this.frames = new ArrayList<>();
            this.timings = new ArrayList<>();
            setFrames(frames, timings);
        }
    
        public boolean isDone() {
            return done.get();
        }
    
        public void reset() {
            totalAnimationDuration = 0;
            done.getAndSet(false);
        }
    
        public void update(long elapsedTime) {
            if (frames.size() > 1) {
                animationTime += elapsedTime;
                if (animationTime >= totalAnimationDuration) {
                    animationTime = animationTime % totalAnimationDuration;
                    currIndex = 0;
                    done.getAndSet(true);
                }
                while (animationTime > timings.get(currIndex)) {
                    currIndex++;
                }
            }
        }
    
        public BufferedImage getCurrentImage() {
            if (frames.isEmpty()) {
                return null;
            } else {
                try {
                    return frames.get(currIndex);
                } catch (Exception ex) {//images might have been altered so we reset the index and return first image of the new frames/animation 
                    currIndex = 0;
                    return frames.get(currIndex);
                }
            }
        }
    
        public void setFrames(ArrayList<BufferedImage> frames, ArrayList< Long> timings) {
            if (frames == null || timings == null) {//so that constructor super(null,null) cause this to throw NullPointerException
                return;
            }
            this.frames.clear();
            for (BufferedImage img : frames) {
                this.frames.add(img);
            }
            this.timings.clear();
            for (long animTime : timings) {
                totalAnimationDuration += animTime;
                this.timings.add(totalAnimationDuration);
            }
        }
    }
    
    abstract class GameLoop {
    
        private AtomicBoolean running, paused;
        private Thread gameLoopThread;
        //how many frames should be drawn in a second
        private final int FRAMES_PER_SECOND;
        //Calculate how many nano seconds each frame should take for our target frames per second.
        private final long TIME_BETWEEN_UPDATES;
        //track number of frames
        private int frameCount;
        //If you're worried about visual hitches more than perfect timing, set this to 1. else 5 should be okay
        private int MAX_UPDATES_BETWEEN_RENDER = 1;
    
        public GameLoop(int fps) {
            FRAMES_PER_SECOND = fps;
            TIME_BETWEEN_UPDATES = 1000000000 / FRAMES_PER_SECOND;
            gameLoopThread = null;
            frameCount = 0;
            running = new AtomicBoolean(false);
            paused = new AtomicBoolean(false);
        }
    
        void start() {
            if (gameLoopThread != null) {
                if (gameLoopThread.isAlive()) {
                    //System.out.println("Thwarted coders attempt to start a gameloop which has already been created");
                    return;
                }
            }
    
            gameLoopThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    gameLoop();
                }
            });
    
            gameLoopThread.start();
            running.set(true);
        }
    
        private void gameLoop() {
            //We will need the last update time.
            long lastUpdateTime = System.nanoTime();
            //store the time we started this will be used for updating map and charcter animations
            long currTime = System.currentTimeMillis();
    
            while (running.get()) {
                if (!paused.get()) {
                    long now = System.nanoTime();
                    long elapsedTime = System.currentTimeMillis() - currTime;
                    currTime += elapsedTime;
    
                    int updateCount = 0;
                    //Do as many game updates as we need to, potentially playing catchup.
                    while (now - lastUpdateTime >= TIME_BETWEEN_UPDATES && updateCount < MAX_UPDATES_BETWEEN_RENDER) {
                        update(elapsedTime);//Update the entity movements and collision checks etc (all has to do with updating the games status i.e  call move() on Enitites)
                        lastUpdateTime += TIME_BETWEEN_UPDATES;
                        updateCount++;
                    }
    
                    //If for some reason an update takes forever, we don't want to do an insane number of catchups.
                    //If you were doing some sort of game that needed to keep EXACT time, you would get rid of this.
                    if (now - lastUpdateTime >= TIME_BETWEEN_UPDATES) {
                        lastUpdateTime = now - TIME_BETWEEN_UPDATES;
                    }
    
                    draw();//draw the game by invokeing repaint (which will call paintComponent) on this JPanel
                    checkCollisions();//check for any collision
    
                    frameCount++;
                    long lastRenderTime = now;
    
                    //Yield until it has been at least the target time between renders. This saves the CPU from hogging.
                    while (now - lastRenderTime < TIME_BETWEEN_UPDATES && now - lastUpdateTime < TIME_BETWEEN_UPDATES) {
                        Thread.yield();
                        now = System.nanoTime();
                    }
                } else {//so we dont eat processor time when paused
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException ex) {
                        //do nothing
                    }
                }
            }
            //System.out.println("GameLoop done");
        }
    
        void pause() {
            paused.set(true);
        }
    
        void stop() {
            paused.set(false);
            running.set(false);
        }
    
        void resume() {
            paused.set(false);
        }
    
        boolean isPaused() {
            return paused.get();
        }
    
        boolean isRunning() {
            return running.get();
        }
    
        abstract void update(long elapsedTime);
    
        abstract void draw();
    
        abstract void checkCollisions();
    }
mymaksimus 0 Newbie Poster

Thank you, really nice ideas!
But ehy you arent using jpanels as game objects? Isnt it a much easier way? Im using jpanel for 2d programming, and i hadnt any problems until now. Do you have any reason against?

JamesCherrill 4,733 Most Valuable Poster Team Colleague Featured Poster

Alternatively... why would you accept all the overhead and potential conflict of using a very large and complex class like JPanel when you don't use any of its methods?

DavidKroukamp 105 Master Poster Team Colleague Featured Poster

But ehy you arent using jpanels as game objects

That is another way, but IMO it will decrease performance if we have many sprites rendering/moving at the same time, thus moving many JPanels etc, it is more performance efficient to draw directly via paintComponent. It also forces us to adopt a Null/Absolute LayoutManager which is really not a good idea, especially if we have a HUD (Head Up Display) than we would have to put the HUD on a glasspane, and redirect mouse events to our game panel etc, so just more work in general.

why would you accept all the overhead and potential conflict of using a very large and complex class like JPanel when you don't use any of its methods?

True James, I should change the perhaps to JComponent, atleast others now know what can be done to improve things

JamesCherrill 4,733 Most Valuable Poster Team Colleague Featured Poster
class GameObject extends Animator {
...
}
class Animator { ...

Right now (unless I'm confused) your game objects don't extend anything, which seems perfectly sensible to me. I certainly wasn't suggesting they should extend JComponent, or any other class.

(Your GamePanel extends JPanel - and I think that's the right thing to do also).

DavidKroukamp 105 Master Poster Team Colleague Featured Poster

Why would you accept all the overhead and potential conflict of using a very large and complex class like JPanel when you don't use any of its methods?

Oops sorry James I thought the above was directed towards my code i.e GamePanel class. But I see it was a reply to previous poster :).

JamesCherrill 4,733 Most Valuable Poster Team Colleague Featured Poster

Yes, that's right.
I did a similar thing for use as a teaching aid in tutorials, and it looks a lot like yours - a JPanel with paintComponent overidden to draw all the game objects, and a hierarchy of game objects that do not need to be JComponents or anything else. Perhaps the only interesting difference is that I separated the behaviour of the game objects from their display - that's because I was modelling things like gravity, air resistance, and lossy bouncing in the objects themselves, and if I put all that with stuff like drawing animated GIFs all in one class it got far too big.

The one area I found really hard was handling collisions between non-rectangular objects, especially when there are lots of objects. It took ages to get an implementation that could show dozens or even hundreds of balls all bouncing off each other.

DavidKroukamp 105 Master Poster Team Colleague Featured Poster

I did a similar thing for use as a teaching aid in tutorials, and it looks a lot like yours - a JPanel with paintComponent overidden to draw all the game objects, and a hierarchy of game objects that do not need to be JComponents or anything else. Perhaps the only interesting difference is that I separated the behaviour of the game objects from their display - that's because I was modelling things like gravity, air resistance, and lossy bouncing in the objects themselves, and if I put all that with stuff like drawing animated GIFs all in one class it got far too big.

The one area I found really hard was handling collisions between non-rectangular objects, especially when there are lots of objects. It took ages to get an implementation that could show dozens or even hundreds of balls all bouncing off each other.

Yes one thing I must still do is OO my logic more i.e seperating the behaviour of the game objects from their display.

As for your project it sounds extremely interesting, I too would like to add physics to the above code, but yeah I think I better do some refreshing on my high school physics lessons first :P.

It would be really awesome if you could post your own code snippet of your game I think it will help many more than mine is right now :)

JamesCherrill 4,733 Most Valuable Poster Team Colleague Featured Poster

The whole thing is quite a heap of code - not sure it's worth posting it all. Any particular areas you think may be interesting?

DavidKroukamp 105 Master Poster Team Colleague Featured Poster

The whole thing is quite a heap of code - not sure it's worth posting it all. Any particular areas you think may be interesting?

Understandbly it should be a heap of code :).

Areas that are interesting?!? Definitely the below:

things like gravity, air resistance, and lossy bouncing

JamesCherrill 4,733 Most Valuable Poster Team Colleague Featured Poster

OK - it starts with the Sprite class -

class Sprite {
   // has basic movement at constant velocity, 
   // a shape (for collisions etc)
   // and a renderer for delegating how it is drawn

   double x, y, dx, dy; // position and velocity (pixels/TIMER_MSEC)
   final Shape shape; // outer boundary of sprite
   final int width, height;
   SpriteRenderer renderer = new DefaultSpriteRenderer();
   AnimationModel model = null;

   public Sprite(double x, double y, double dx, double dy, Shape shape) {
      // initial position and velocity
      this.x = x;
      this.y = y;
      this.dx = dx;
      this.dy = dy;
      this.shape = shape;
      width = shape.getBounds().width;
      height = shape.getBounds().height;
   }

   public void update() { // update position and velocity every n milliSec
      // default - just move at current velocity
      x += dx; // velocity in x direction
      y += dy; // velocity in y direction
      // delete sprite if it has gone off RHS or LHS of model
      if ((dx > 0 && x > model.getWidth()) || (dx < 0 && (x + width) < 0)) {
         model.remove(this);
      }

    (etc etc)

update() is called at regular intervals by a java.util.Timer's scheduleAtFixedRate

Then various sub-classes model balloons (negative weight, lots of air resistance, burst on collision), bricks (heavy, negligable air resistance, don't bounce), balls (a bit of everything). The Ball update method is simply (using constants for drag etc, just for illustration)

   @Override
   public void update() { // update velocity every n milliSec
      dy = dy + 1; // acceleration due to gravity
      dx *= .98; // air resistance
      dy *= .98;
      super.update(); // move according to latest velocity
      if (y >= model.getHeight() - height) {
         // ball bounces imperfectly when it hits the floor
         dy = -dy * 0.9f;
         y = model.getHeight() - height;
      }
   }

Is any of that helpful?

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.