Search code examples
javaswingnullpaintcomponentkey-bindings

Program not painting screen properly


I've been constructing a short program that basically draws a spaceship on a JPanel and listens for keys that tell the program to shoot a bullet. The problem is that it's not even painting the spaceship or the bullets on the screen. I also suspect that the KeyBindings may not be working as that was a previous problem (that I may or may not have fixed), but the main issue at hand is still the fact that my screen isn't being painted. Here is my code:

public enum Direction {
    LEFT, RIGHT, SPACE
}

import javax.swing.JFrame;

public class Main {
    public static void main(String[] args) {
        JFrame frame;

        Ship s1;
        Shoot shoot;

        // Set the frame up
        frame = new JFrame();
        frame.setSize(400, 300);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setResizable(false);
        frame.setVisible(true);

        // Get some more necessary objects
        s1 = new Ship();
        shoot = new Shoot(s1);
        frame.getContentPane().add(shoot);
        s1.setShoot(shoot);

        // Threads
        Thread ship = new Thread(s1);
        ship.start();
    }
}

import java.awt.Graphics;
import java.awt.event.KeyEvent;

import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JPanel;
import javax.swing.KeyStroke;

public class Shoot extends JPanel {

    Ship s1;

    public Shoot(Ship s1) {
        this.s1 = s1;

        addKeyBinding(KeyEvent.VK_LEFT, "left.pressed", new MoveAction(true, s1, Direction.LEFT), true);
        addKeyBinding(KeyEvent.VK_LEFT, "left.released", new MoveAction(false, s1, Direction.LEFT), false);

        addKeyBinding(KeyEvent.VK_RIGHT, "right.pressed", new MoveAction(true, s1, Direction.RIGHT), true);
        addKeyBinding(KeyEvent.VK_RIGHT, "right.released", new MoveAction(false, s1, Direction.RIGHT), false);

        addKeyBinding(KeyEvent.VK_SPACE, "space.pressed", new MoveAction(true, s1, Direction.SPACE), true);
        addKeyBinding(KeyEvent.VK_SPACE, "space.released", new MoveAction(false, s1, Direction.SPACE), false);

        setDoubleBuffered(true);
    }

    @Override
    public void paintComponent(Graphics g) {
        // Draw the ship
        super.paintComponent(g);
        s1.draw(g);
        g.fill3DRect(40, 50, 10, 10, false);
    }

    protected void addKeyBinding(int keyCode, String name, Action action, boolean keyPressed) {
        if (keyPressed) {
            addKeyBinding(KeyStroke.getKeyStroke(keyCode, 0, false), name, action);
        } else {
            addKeyBinding(KeyStroke.getKeyStroke(keyCode, 0, true), name, action);
        }
    }

    protected void addKeyBinding(KeyStroke keyStroke, String name, Action action) {
        InputMap inputMap = getInputMap(WHEN_IN_FOCUSED_WINDOW);
        ActionMap actionMap = getActionMap();
        inputMap.put(keyStroke, name);
        actionMap.put(name, action);
    }

}

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Rectangle;

public class Ship implements Runnable {
    int x, y, xDirection, bx, by;
    boolean readyToFire, shooting = false;
    Rectangle bullet;
    Shoot shoot;

    public Ship() {
        x = 175;
        y = 275;
        bullet = new Rectangle(0, 0, 3, 5);
    }

    public void draw(Graphics g) {
        // System.out.println("draw() called");
        g.setColor(Color.BLUE);
        g.fillRect(x, y, 40, 10);
        g.fillRect(x + 18, y - 7, 4, 7);
        if (shooting) {
            g.setColor(Color.RED);
            g.fillRect(bullet.x, bullet.y, bullet.width, bullet.height);
        }
        shoot.repaint();
    }

    public void move() {
        x += xDirection;
        if (x <= 0)
            x = 0;
        if (x >= 360)
            x = 360;
        shoot.repaint();
    }

    public void shoot() {
        if (shooting) {
            bullet.y--;
            shoot.repaint();
        }
    }

    public void setXDirection(int xdir) {
        xDirection = xdir;
    }

    public void setShoot(Shoot shoot) {
        this.shoot = shoot;
    }

    @Override
    public void run() {
        try {
            while (true) {
                shoot();
                move();
                Thread.sleep(5);
            }
        } catch (Exception e) {
            System.err.println(e.getMessage());
        }

    }
}

import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.util.HashSet;

import javax.swing.AbstractAction;

public class MoveAction extends AbstractAction {

    boolean pressed;
    Ship s1;
    Direction dir;
    private Set<Direction> movement;

    public MoveAction(boolean pressed, Ship s1, Direction dir) {
        System.out.println("moveaction class");
        movement = new HashSet<Direction>();
        this.pressed = pressed;
        this.s1 = s1;
        this.dir = dir;
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        try {
            if (movement.contains(Direction.LEFT)) {
                if (pressed) {
                    s1.setXDirection(-1);
                } else {
                    s1.setXDirection(0);
                }
            } else if (movement.contains(Direction.RIGHT)) {
                if (pressed) {
                    s1.setXDirection(1);
                } else {
                    s1.setXDirection(0);
                }
            } else if (movement.contains(Direction.SPACE)) {
                if (pressed) {
                    if (s1.bullet == null)
                        s1.readyToFire = true;
                    if (s1.readyToFire) {
                        s1.bullet.x = s1.x + 18;
                        s1.bullet.y = s1.y - 7;
                        s1.shooting = true;
                    }
                } else {
                    s1.readyToFire = false;
                    if (s1.bullet.y <= -7) {
                        s1.bullet = null;
                        s1.shooting = false;
                        s1.bullet = null;
                        s1.bullet = new Rectangle(0, 0, 0, 0);
                        s1.readyToFire = true;
                    }
                }
            }
        } catch (NullPointerException ex) {
            System.out.println("NullPointerException");
        }
    }

Solution

  • So, there are a number of issues...

    You should call setVisible on the JFrame last, this will ensure that components are laid out

    Your keybindings issue seem to related to the fact that you're using the movement Set, but you never actually add anything to it. Instead you should be checking the dir value.

    And probably a bunch of other things. Your code is tightly coupled and there isn't any centralised management of the state.

    I'd start by having a better understand of the Model-View-Controller paradigm and separate the code into appropriate areas of responsibility.

    The "data" for the game should separate from the "rendering" of the game, which should be separate from the decisions about how the game is to be updated.

    What I'm about to present is an oversimplification intended to spark ideas, rather than been a concrete solution, as there are a number of ways you could achieve the physical implementation...

    So, what we need is, a concept of something in the game, AKA an "entity", entities take many forms, but I'm going to focus on renderable/displayable entities. You need some kind of model that is responsible for modeling the current state of the game and which is responsible for implementing the rules. You need some kind of view, which is responsible for rendering the model and responding to user input. You need some kind of controller which controls how the model and the view bridged.

    It's always a good idea to start with a series of interfaces which define the contractual expectations and outlines the expected means by which elements are expected to communicate with each other. Again, I've taken a simple, direct approach, but it's by no means the only...

    public static enum Direction {
        LEFT,
        RIGHT,
        SPACE
    }
    
    public interface Entity {
        public void paint(Graphics2D g2d);
        public Point getLocation();
        public void setLocation(Point p);
        public Dimension getSize();
    }
    
    public interface GameModel {
        public Entity[] getEntities();
        public void update(Rectangle bounds, Set<Direction> keys);
    }
    
    public interface GameController {
        public Entity[] getEntities();
        public void setDirection(Direction direction, boolean pressed);
        public void start();
    }
    
    public interface GameView {
        public void setController(GameController controller);
        public GameController getController();
        public Rectangle getViewBounds();
        public void repaint();
    }
    

    Let's take a look at the implementation of the entities. This example has two entities, a Player and a Bullet...

    public abstract class AbstractEntity implements Entity {
    
        private final Point location = new Point(0, 0);
    
        @Override
        public Point getLocation() {
            return new Point(location);
        }
    
        @Override
        public void setLocation(Point p) {
            location.setLocation(p);
        }
    
    }
    
    public class Player extends AbstractEntity {
    
        public Player(Rectangle bounds) {
            int x = bounds.x + ((bounds.width - getSize().width) / 2);
            int y = bounds.y + (bounds.height - getSize().height);
            setLocation(new Point(x, y));
        }
    
        @Override
        public Dimension getSize() {
            return new Dimension(40, 17);
        }
    
        @Override
        public void paint(Graphics2D g2d) {
            Point p = getLocation();
            Dimension size = getSize();
            g2d.setColor(Color.BLUE);
            g2d.fillRect(p.x, p.y + 7, size.width, 10);
            g2d.fillRect(p.x + 18, p.y, 4, 7);
        }
    
    }
    
    public class Bullet extends AbstractEntity {
    
        @Override
        public void paint(Graphics2D g2d) {
            Rectangle bullet = new Rectangle(getLocation(), getSize());
            g2d.setColor(Color.RED);
            g2d.fill(bullet);
        }
    
        @Override
        public Dimension getSize() {
            return new Dimension(4, 8);
        }
    
    }
    

    Nothing spectacular, but they each define their parameters and can render their states when asked.

    Next, we have the model, controller and view. This example uses the controller as the primary game loop, responsible for updating the game (model) state and scheduling repaints. This is done with the use of a Swing Timer as this prevents possible race conditions between the update loop and the painting loop. A more complex implementation would need to take over the painting process through the use of a BufferStrategy and BufferStrategy and BufferCapabilities.

    The model simply takes the current view boundaries and the current state of the keys and updates the state of the entities.

    The view monitors user input, notifying the controller, and renders the current state of the game.

    You'll note that the view and model never communicate directly with each other, this is the domain of the controller.

    public class DefaultGameModel implements GameModel {
    
        private final List<Entity> entities;
        private Player player;
    
        private Long lastShot;
    
        public DefaultGameModel() {
            entities = new ArrayList<>(25);
        }
    
        @Override
        public Entity[] getEntities() {
            return entities.toArray(new Entity[0]);
        }
    
        @Override
        public void update(Rectangle bounds, Set<Direction> keys) {
            if (player == null) {
                player = new Player(bounds);
                entities.add(player);
            }
    
            Point p = player.getLocation();
            int xDelta = 0;
            if (keys.contains(Direction.LEFT)) {
                xDelta = -4;
            } else if (keys.contains(Direction.RIGHT)) {
                xDelta = 4;
            }
            p.x += xDelta;
            if (p.x <= bounds.x) {
                p.x = bounds.x;
            } else if (p.x + player.getSize().width >= bounds.x + bounds.width) {
                p.x = bounds.width - player.getSize().width;
            }
            player.setLocation(p);
    
            Iterator<Entity> it = entities.iterator();
            while (it.hasNext()) {
                Entity entity = it.next();
                if (entity instanceof Bullet) {
                    Point location = entity.getLocation();
                    Dimension size = entity.getSize();
                    location.y -= size.height;
                    if (location.y + size.height < bounds.y) {
                        it.remove();
                    } else {
                        entity.setLocation(location);
                    }
                }
            }
    
            if (keys.contains(Direction.SPACE)) {
                if (lastShot == null || System.currentTimeMillis() - lastShot > 100) {
                    lastShot = System.currentTimeMillis();
                    Bullet bullet = new Bullet();
                    int x = p.x + ((player.getSize().width - bullet.getSize().width) / 2);
                    int y = p.y - bullet.getSize().height;
                    bullet.setLocation(new Point(x, y));
    
                    entities.add(bullet);
                }
            }
        }
    
    }
    
    public class DefaultGameController implements GameController {
    
        private GameModel model;
        private GameView view;
    
        private Timer timer;
    
        private Set<Direction> keys = new HashSet<>(25);
    
        public DefaultGameController(GameModel gameModel, GameView gameView) {
            gameView.setController(this);
    
            view = gameView;
            model = gameModel;
        }
    
        @Override
        public Entity[] getEntities() {
            return model.getEntities();
        }
    
        @Override
        public void setDirection(Direction direction, boolean pressed) {
            if (pressed) {
                keys.add(direction);
            } else {
                keys.remove(direction);
            }
        }
    
        @Override
        public void start() {
            if (timer != null && timer.isRunning()) {
                timer.stop();
            }
            timer = new Timer(40, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    model.update(view.getViewBounds(), Collections.unmodifiableSet(keys));
                    view.repaint();
                }
            });
            timer.start();
        }
    
    }
    
    public class DefaultGameView extends JPanel implements GameView {
    
        private GameController controller;
    
        public DefaultGameView() {
            addKeyBinding("left.pressed", KeyEvent.VK_LEFT, true, new DirectionAction(Direction.LEFT, true));
            addKeyBinding("left.released", KeyEvent.VK_LEFT, false, new DirectionAction(Direction.LEFT, false));
            addKeyBinding("right.pressed", KeyEvent.VK_RIGHT, true, new DirectionAction(Direction.RIGHT, true));
            addKeyBinding("right.released", KeyEvent.VK_RIGHT, false, new DirectionAction(Direction.RIGHT, false));
            addKeyBinding("space.pressed", KeyEvent.VK_SPACE, true, new DirectionAction(Direction.SPACE, true));
            addKeyBinding("space.released", KeyEvent.VK_SPACE, false, new DirectionAction(Direction.SPACE, false));
        }
    
        protected void addKeyBinding(String name, int keyEvent, boolean pressed, DirectionAction action) {
            addKeyBinding(name, KeyStroke.getKeyStroke(keyEvent, 0, !pressed), action);
        }
    
        protected void addKeyBinding(String name, KeyStroke keyStroke, DirectionAction action) {
            InputMap inputMap = getInputMap(WHEN_IN_FOCUSED_WINDOW);
            ActionMap actionMap = getActionMap();
    
            inputMap.put(keyStroke, name);
            actionMap.put(name, action);
        }
    
        @Override
        public void setController(GameController controller) {
            this.controller = controller;
        }
    
        @Override
        public GameController getController() {
            return controller;
        }
    
        @Override
        public Rectangle getViewBounds() {
            return new Rectangle(new Point(0, 0), getSize());
        }
    
        @Override
        public Dimension getPreferredSize() {
            return new Dimension(400, 400);
        }
    
        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            GameController controller = getController();
            for (Entity entity : controller.getEntities()) {
                // I don't trust you
                Graphics2D g2d = (Graphics2D) g.create();
                entity.paint(g2d);
                g2d.dispose();
            }
        }
    
        public class DirectionAction extends AbstractAction {
    
            private Direction direction;
            private boolean pressed;
    
            public DirectionAction(Direction direction, boolean pressed) {
                this.direction = direction;
                this.pressed = pressed;
            }
    
            @Override
            public void actionPerformed(ActionEvent e) {
                getController().setDirection(direction, pressed);
            }
    
        }
    
    }
    

    Okay, but that's all fine and good, but how do you use it? Something like this for example...

    Play

    import java.awt.Color;
    import java.awt.Dimension;
    import java.awt.EventQueue;
    import java.awt.Graphics;
    import java.awt.Graphics2D;
    import java.awt.Point;
    import java.awt.Rectangle;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.awt.event.KeyEvent;
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.HashSet;
    import java.util.Iterator;
    import java.util.List;
    import java.util.Set;
    import javax.swing.AbstractAction;
    import javax.swing.ActionMap;
    import javax.swing.InputMap;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    import javax.swing.KeyStroke;
    import javax.swing.Timer;
    import javax.swing.UIManager;
    import javax.swing.UnsupportedLookAndFeelException;
    
    public class Main {
    
        public static void main(String[] args) {
            new Main();
        }
    
        public Main() {
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    try {
                        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                    } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                        ex.printStackTrace();
                    }
    
                    GameModel model = new DefaultGameModel();
                    DefaultGameView view = new DefaultGameView();
                    GameController controller = new DefaultGameController(model, view);
    
                    JFrame frame = new JFrame("Testing");
                    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                    frame.add(view);
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
    
                    controller.start();
                }
            });
        }
    
        public static enum Direction {
            LEFT,
            RIGHT,
            SPACE
        }
    
        public interface Entity {
            public void paint(Graphics2D g2d);
            public Point getLocation();
            public void setLocation(Point p);
            public Dimension getSize();
        }
    
        public interface GameModel {
            public Entity[] getEntities();
            public void update(Rectangle bounds, Set<Direction> keys);
        }
    
        public interface GameController {
            public Entity[] getEntities();
            public void setDirection(Direction direction, boolean pressed);
            public void start();
        }
    
        public interface GameView {
            public void setController(GameController controller);
            public GameController getController();
            public Rectangle getViewBounds();
            public void repaint();
        }
    
        public class DefaultGameModel implements GameModel {
    
            private final List<Entity> entities;
            private Player player;
    
            private Long lastShot;
    
            public DefaultGameModel() {
                entities = new ArrayList<>(25);
            }
    
            @Override
            public Entity[] getEntities() {
                return entities.toArray(new Entity[0]);
            }
    
            @Override
            public void update(Rectangle bounds, Set<Direction> keys) {
                if (player == null) {
                    player = new Player(bounds);
                    entities.add(player);
                }
    
                Point p = player.getLocation();
                int xDelta = 0;
                if (keys.contains(Direction.LEFT)) {
                    xDelta = -4;
                } else if (keys.contains(Direction.RIGHT)) {
                    xDelta = 4;
                }
                p.x += xDelta;
                if (p.x <= bounds.x) {
                    p.x = bounds.x;
                } else if (p.x + player.getSize().width >= bounds.x + bounds.width) {
                    p.x = bounds.width - player.getSize().width;
                }
                player.setLocation(p);
    
                Iterator<Entity> it = entities.iterator();
                while (it.hasNext()) {
                    Entity entity = it.next();
                    if (entity instanceof Bullet) {
                        Point location = entity.getLocation();
                        Dimension size = entity.getSize();
                        location.y -= size.height;
                        if (location.y + size.height < bounds.y) {
                            it.remove();
                        } else {
                            entity.setLocation(location);
                        }
                    }
                }
    
                if (keys.contains(Direction.SPACE)) {
                    if (lastShot == null || System.currentTimeMillis() - lastShot > 100) {
                        lastShot = System.currentTimeMillis();
                        Bullet bullet = new Bullet();
                        int x = p.x + ((player.getSize().width - bullet.getSize().width) / 2);
                        int y = p.y - bullet.getSize().height;
                        bullet.setLocation(new Point(x, y));
    
                        entities.add(bullet);
                    }
                }
            }
    
        }
    
        public class DefaultGameController implements GameController {
    
            private GameModel model;
            private GameView view;
    
            private Timer timer;
    
            private Set<Direction> keys = new HashSet<>(25);
    
            public DefaultGameController(GameModel gameModel, GameView gameView) {
                gameView.setController(this);
    
                view = gameView;
                model = gameModel;
            }
    
            @Override
            public Entity[] getEntities() {
                return model.getEntities();
            }
    
            @Override
            public void setDirection(Direction direction, boolean pressed) {
                if (pressed) {
                    keys.add(direction);
                } else {
                    keys.remove(direction);
                }
            }
    
            @Override
            public void start() {
                if (timer != null && timer.isRunning()) {
                    timer.stop();
                }
                timer = new Timer(40, new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        model.update(view.getViewBounds(), Collections.unmodifiableSet(keys));
                        view.repaint();
                    }
                });
                timer.start();
            }
    
        }
    
        public abstract class AbstractEntity implements Entity {
    
            private final Point location = new Point(0, 0);
    
            @Override
            public Point getLocation() {
                return new Point(location);
            }
    
            @Override
            public void setLocation(Point p) {
                location.setLocation(p);
            }
    
        }
    
        public class Player extends AbstractEntity {
    
            public Player(Rectangle bounds) {
                int x = bounds.x + ((bounds.width - getSize().width) / 2);
                int y = bounds.y + (bounds.height - getSize().height);
                setLocation(new Point(x, y));
            }
    
            @Override
            public Dimension getSize() {
                return new Dimension(40, 17);
            }
    
            @Override
            public void paint(Graphics2D g2d) {
                Point p = getLocation();
                Dimension size = getSize();
                g2d.setColor(Color.BLUE);
                g2d.fillRect(p.x, p.y + 7, size.width, 10);
                g2d.fillRect(p.x + 18, p.y, 4, 7);
            }
    
        }
    
        public class Bullet extends AbstractEntity {
    
            @Override
            public void paint(Graphics2D g2d) {
                Rectangle bullet = new Rectangle(getLocation(), getSize());
                g2d.setColor(Color.RED);
                g2d.fill(bullet);
            }
    
            @Override
            public Dimension getSize() {
                return new Dimension(4, 8);
            }
    
        }
    
        public class DefaultGameView extends JPanel implements GameView {
    
            private GameController controller;
    
            public DefaultGameView() {
                addKeyBinding("left.pressed", KeyEvent.VK_LEFT, true, new DirectionAction(Direction.LEFT, true));
                addKeyBinding("left.released", KeyEvent.VK_LEFT, false, new DirectionAction(Direction.LEFT, false));
                addKeyBinding("right.pressed", KeyEvent.VK_RIGHT, true, new DirectionAction(Direction.RIGHT, true));
                addKeyBinding("right.released", KeyEvent.VK_RIGHT, false, new DirectionAction(Direction.RIGHT, false));
                addKeyBinding("space.pressed", KeyEvent.VK_SPACE, true, new DirectionAction(Direction.SPACE, true));
                addKeyBinding("space.released", KeyEvent.VK_SPACE, false, new DirectionAction(Direction.SPACE, false));
            }
    
            protected void addKeyBinding(String name, int keyEvent, boolean pressed, DirectionAction action) {
                addKeyBinding(name, KeyStroke.getKeyStroke(keyEvent, 0, !pressed), action);
            }
    
            protected void addKeyBinding(String name, KeyStroke keyStroke, DirectionAction action) {
                InputMap inputMap = getInputMap(WHEN_IN_FOCUSED_WINDOW);
                ActionMap actionMap = getActionMap();
    
                inputMap.put(keyStroke, name);
                actionMap.put(name, action);
            }
    
            @Override
            public void setController(GameController controller) {
                this.controller = controller;
            }
    
            @Override
            public GameController getController() {
                return controller;
            }
    
            @Override
            public Rectangle getViewBounds() {
                return new Rectangle(new Point(0, 0), getSize());
            }
    
            @Override
            public Dimension getPreferredSize() {
                return new Dimension(400, 400);
            }
    
            @Override
            protected void paintComponent(Graphics g) {
                super.paintComponent(g);
                GameController controller = getController();
                for (Entity entity : controller.getEntities()) {
                    // I don't trust you
                    Graphics2D g2d = (Graphics2D) g.create();
                    entity.paint(g2d);
                    g2d.dispose();
                }
            }
    
            public class DirectionAction extends AbstractAction {
    
                private Direction direction;
                private boolean pressed;
    
                public DirectionAction(Direction direction, boolean pressed) {
                    this.direction = direction;
                    this.pressed = pressed;
                }
    
                @Override
                public void actionPerformed(ActionEvent e) {
                    getController().setDirection(direction, pressed);
                }
    
            }
    
        }
    
    }
    

    Again, you're going to need to go away and do some more research, but this is the general idea