How to make an image move while listening to a keypress in Java.

I'm starting to learn java programming and I think it's cool to learn java through game development. I know how to draw image and listen to a keypress then move that image. But is it possible to make the image move back and forth to the window while the window is listening to a keypress? Like for example, while the image or object(like spaceship) is moving left to right in the window, then if I press space key, a laser will fire at the bottom of the screen( cool huh :D ). But basically I just want to know how to make the image move left to right while the window is listening to a keypress.

I'm thinking that I will add a key listener to my window then fire an infinite loop to move the image. Or do I need to learn about threading so that another thread will move the object?

Please advise.

Many thanks.


Solution 1:

Yep, a Swing Timer and Key Bindings would work well. Here's another example (mine) :)

import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import javax.swing.*;

public class AnimationWithKeyBinding {
   private static void createAndShowUI() {
      AnimationPanel panel = new AnimationPanel(); // the drawing JPanel

      JFrame frame = new JFrame("Animation With Key Binding");
      frame.getContentPane().add(panel);
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      frame.pack();
      frame.setLocationRelativeTo(null);
      frame.setVisible(true);
   }

   public static void main(String[] args) {
      java.awt.EventQueue.invokeLater(new Runnable() {
         public void run() {
            createAndShowUI();
         }
      });
   }
}

@SuppressWarnings("serial")
class AnimationPanel extends JPanel {
   public static final int SPRITE_WIDTH = 20;
   public static final int PANEL_WIDTH = 400;
   public static final int PANEL_HEIGHT = 400;
   private static final int MAX_MSTATE = 25;
   private static final int SPIN_TIMER_PERIOD = 16;
   private static final int SPRITE_STEP = 3;

   private int mState = 0;
   private int mX = (PANEL_WIDTH - SPRITE_WIDTH) / 2;
   private int mY = (PANEL_HEIGHT - SPRITE_WIDTH) / 2;
   private int oldMX = mX;
   private int oldMY = mY;
   private boolean moved = false;

   // an array of sprite images that are drawn sequentially
   private BufferedImage[] spriteImages = new BufferedImage[MAX_MSTATE];

   public AnimationPanel() {
      // create and start the main animation timer
      new Timer(SPIN_TIMER_PERIOD, new SpinTimerListener()).start();
      setPreferredSize(new Dimension(PANEL_WIDTH, PANEL_HEIGHT));
      setBackground(Color.white);
      createSprites(); // create the images
      setupKeyBinding();
   }

   private void setupKeyBinding() {
      int condition = JComponent.WHEN_IN_FOCUSED_WINDOW;
      InputMap inMap = getInputMap(condition);
      ActionMap actMap = getActionMap();

      // this uses an enum of Direction that holds ints for the arrow keys
      for (Direction direction : Direction.values()) {
         int key = direction.getKey();
         String name = direction.name();

         // add the key bindings for arrow key and shift-arrow key
         inMap.put(KeyStroke.getKeyStroke(key, 0), name);
         inMap.put(KeyStroke.getKeyStroke(key, InputEvent.SHIFT_DOWN_MASK), name);
         actMap.put(name, new MyKeyAction(this, direction));
      }
   }

   // create a bunch of buffered images and place into an array,
   // to be displayed sequentially
   private void createSprites() {
      for (int i = 0; i < spriteImages.length; i++) {
         spriteImages[i] = new BufferedImage(SPRITE_WIDTH, SPRITE_WIDTH,
                  BufferedImage.TYPE_INT_ARGB);
         Graphics2D g2 = spriteImages[i].createGraphics();
         g2.setColor(Color.red);
         g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
         double theta = i * Math.PI / (2 * spriteImages.length);
         double x = SPRITE_WIDTH * Math.abs(Math.cos(theta)) / 2.0;
         double y = SPRITE_WIDTH * Math.abs(Math.sin(theta)) / 2.0;
         int x1 = (int) ((SPRITE_WIDTH / 2.0) - x);
         int y1 = (int) ((SPRITE_WIDTH / 2.0) - y);
         int x2 = (int) ((SPRITE_WIDTH / 2.0) + x);
         int y2 = (int) ((SPRITE_WIDTH / 2.0) + y);
         g2.drawLine(x1, y1, x2, y2);
         g2.drawLine(y1, x2, y2, x1);
         g2.dispose();
      }
   }

   @Override
   protected void paintComponent(Graphics g) {
      super.paintComponent(g);
      g.drawImage(spriteImages[mState], mX, mY, null);
   }

   public void incrementX(boolean right) {
      oldMX = mX;
      if (right) {
         mX = Math.min(getWidth() - SPRITE_WIDTH, mX + SPRITE_STEP);
      } else {
         mX = Math.max(0, mX - SPRITE_STEP);
      }
      moved = true;
   }

   public void incrementY(boolean down) {
      oldMY = mY;
      if (down) {
         mY = Math.min(getHeight() - SPRITE_WIDTH, mY + SPRITE_STEP);
      } else {
         mY = Math.max(0, mY - SPRITE_STEP);
      }
      moved = true;
   }

   public void tick() {
      mState = (mState + 1) % MAX_MSTATE;
   }

   private class SpinTimerListener implements ActionListener {
      @Override
      public void actionPerformed(ActionEvent e) {
         tick();

         int delta = 20;
         int width = SPRITE_WIDTH + 2 * delta;
         int height = width;

         // make sure to erase the old image
         if (moved) {
            int x = oldMX - delta;
            int y = oldMY - delta;
            repaint(x, y, width, height);
         }

         int x = mX - delta;
         int y = mY - delta;

         // draw the new image
         repaint(x, y, width, height);
         moved = false;
      }
   }
}

enum Direction {
   UP(KeyEvent.VK_UP), DOWN(KeyEvent.VK_DOWN), LEFT(KeyEvent.VK_LEFT), RIGHT(KeyEvent.VK_RIGHT);

   private int key;

   private Direction(int key) {
      this.key = key;
   }

   public int getKey() {
      return key;
   }
}

// Actions for the key binding
@SuppressWarnings("serial")
class MyKeyAction extends AbstractAction {
   private AnimationPanel draw;
   private Direction direction;

   public MyKeyAction(AnimationPanel draw, Direction direction) {
      this.draw = draw;
      this.direction = direction;
   }

   @Override
   public void actionPerformed(ActionEvent e) {
      switch (direction) {
      case UP:
         draw.incrementY(false);
         break;
      case DOWN:
         draw.incrementY(true);
         break;
      case LEFT:
         draw.incrementX(false);
         break;
      case RIGHT:
         draw.incrementX(true);
         break;

      default:
         break;
      }
   }
}



Here is another example that uses this sprite sheet:

enter image description here

obtained from this site.

Again it's an example of drawing within a JPanel's paintComponent method and using Key Bindings to tell which direction to move.

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;

import javax.imageio.ImageIO;
import javax.swing.*;

@SuppressWarnings("serial")
public class Mcve3 extends JPanel {
    private static final int PREF_W = 800;
    private static final int PREF_H = 640;
    private static final int TIMER_DELAY = 50;

    private int spriteX = 400;
    private int spriteY = 320;
    private SpriteDirection spriteDirection = SpriteDirection.RIGHT;
    private MySprite sprite = null;
    private Timer timer = null;

    public Mcve3() {
        try {
            sprite = new MySprite(spriteDirection, spriteX, spriteY);
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(-1);
        }
        setBackground(Color.WHITE);

        setKeyBindings(SpriteDirection.LEFT, KeyEvent.VK_LEFT);
        setKeyBindings(SpriteDirection.RIGHT, KeyEvent.VK_RIGHT);
        setKeyBindings(SpriteDirection.FORWARD, KeyEvent.VK_DOWN);
        setKeyBindings(SpriteDirection.AWAY, KeyEvent.VK_UP);

        timer = new Timer(TIMER_DELAY, new TimerListener());
        timer.start();
    }

    private void setKeyBindings(SpriteDirection dir, int keyCode) {
        int condition = WHEN_IN_FOCUSED_WINDOW;
        InputMap inputMap = getInputMap(condition);
        ActionMap actionMap = getActionMap();

        KeyStroke keyPressed = KeyStroke.getKeyStroke(keyCode, 0, false);
        KeyStroke keyReleased = KeyStroke.getKeyStroke(keyCode, 0, true);

        inputMap.put(keyPressed, keyPressed.toString());
        inputMap.put(keyReleased, keyReleased.toString());

        actionMap.put(keyPressed.toString(), new MoveAction(dir, false));
        actionMap.put(keyReleased.toString(), new MoveAction(dir, true));
    }

    @Override
    public Dimension getPreferredSize() {
        if (isPreferredSizeSet()) {
            return super.getPreferredSize();
        }
        return new Dimension(PREF_W, PREF_H);
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        sprite.draw(g);
    }

    private class MoveAction extends AbstractAction {
        private SpriteDirection dir;
        private boolean released;

        public MoveAction(SpriteDirection dir, boolean released) {
            this.dir = dir;
            this.released = released;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            if (released) {
                sprite.setMoving(false);
            } else {
                sprite.setMoving(true);
                sprite.setDirection(dir);
            }
        }
    }

    private class TimerListener implements ActionListener {
        @Override
            public void actionPerformed(ActionEvent e) {
                if (sprite.isMoving()) {
                    sprite.tick();
                }
                repaint();
            }
    }

    private static void createAndShowGui() {
        Mcve3 mainPanel = new Mcve3();

        JFrame frame = new JFrame("MCVE");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(mainPanel);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> createAndShowGui());
    }
}

class MySprite {
    private static final String SPRITE_SHEET_PATH = "http://"
            + "orig12.deviantart.net/7db3/f/2010/338/3/3/"
            + "animated_sprite_sheet_32x32_by_digibody-d3479l2.gif";
    private static final int MAX_MOVING_INDEX = 4;
    private static final int DELTA = 4;
    private SpriteDirection direction;
    private Map<SpriteDirection, Image> standingImgMap = new EnumMap<>(SpriteDirection.class);
    private Map<SpriteDirection, List<Image>> movingImgMap = new EnumMap<>(SpriteDirection.class);
    private int x;
    private int y;
    private boolean moving = false;
    private int movingIndex = 0;

    public MySprite(SpriteDirection direction, int x, int y) throws IOException {
        this.direction = direction;
        this.x = x;
        this.y = y;
        createSprites();
    }

    public void draw(Graphics g) {
        Image img = null;
        if (!moving) {
            img = standingImgMap.get(direction);
        } else {
            img = movingImgMap.get(direction).get(movingIndex);
        }
        g.drawImage(img, x, y, null);
    }

    private void createSprites() throws IOException {
        URL spriteSheetUrl = new URL(SPRITE_SHEET_PATH);
        BufferedImage img = ImageIO.read(spriteSheetUrl);

        // get sub-images (sprites) from the sprite sheet
        // magic numbers for getting sprites from sheet, all obtained by trial and error
        int x0 = 0;
        int y0 = 64;
        int rW = 32;
        int rH = 32;
        for (int row = 0; row < 4; row++) {
            SpriteDirection dir = SpriteDirection.values()[row];
            List<Image> imgList = new ArrayList<>();
            movingImgMap.put(dir, imgList);
            int rY = y0 + row * rH;
            for (int col = 0; col < 5; col++) {
                int rX = x0 + col * rW;
                BufferedImage subImg = img.getSubimage(rX, rY, rW, rH);
                if (col == 0) {
                    // first image is standing
                    standingImgMap.put(dir, subImg);
                } else {
                    // all others are moving
                    imgList.add(subImg);
                }
            }
        }
    }

    public SpriteDirection getDirection() {
        return direction;
    }

    public void setDirection(SpriteDirection direction) {
        if (this.direction != direction) {
            setMoving(false);
        }
        this.direction = direction;

    }

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getY() {
        return y;
    }

    public void setY(int y) {
        this.y = y;
    }

    public boolean isMoving() {
        return moving;
    }

    public void setMoving(boolean moving) {
        this.moving = moving;
        if (!moving) {
            movingIndex = 0;
        }
    }

    public void tick() {
        if (moving) {
            switch (direction) {
            case RIGHT:
                x += DELTA;
                break;
            case LEFT:
                x -= DELTA;
                break;
            case FORWARD:
                y += DELTA;
                break;
            case AWAY:
                y -= DELTA;
            }
            movingIndex++;
            movingIndex %= MAX_MOVING_INDEX;
        }
    }

    public int getMovingIndex() {
        return movingIndex;
    }

    public void setMovingIndex(int movingIndex) {
        this.movingIndex = movingIndex;
    }

}

enum SpriteDirection {
    FORWARD, LEFT, AWAY, RIGHT
}

Solution 2:

As an alternative to KeyListener, consider using actions and key bindings, discussed here. Derived from this example, the program below moves a line left, down, up or right using either buttons or keys.

import java.awt.BasicStroke;
import java.awt.BorderLayout;
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.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;

/**
 * @see https://stackoverflow.com/questions/6991648
 * @see https://stackoverflow.com/questions/6887296
 * @see https://stackoverflow.com/questions/5797965
 */
public class LinePanel extends JPanel {

    private MouseHandler mouseHandler = new MouseHandler();
    private Point p1 = new Point(100, 100);
    private Point p2 = new Point(540, 380);
    private boolean drawing;

    public LinePanel() {
        this.setPreferredSize(new Dimension(640, 480));
        this.addMouseListener(mouseHandler);
        this.addMouseMotionListener(mouseHandler);
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D) g;
        g2d.setColor(Color.blue);
        g2d.setRenderingHint(
            RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setStroke(new BasicStroke(8,
            BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL));
        g.drawLine(p1.x, p1.y, p2.x, p2.y);
    }

    private class MouseHandler extends MouseAdapter {

        @Override
        public void mousePressed(MouseEvent e) {
            drawing = true;
            p1 = e.getPoint();
            p2 = p1;
            repaint();
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            drawing = false;
            p2 = e.getPoint();
            repaint();
        }

        @Override
        public void mouseDragged(MouseEvent e) {
            if (drawing) {
                p2 = e.getPoint();
                repaint();
            }
        }
    }

    private class ControlPanel extends JPanel {

        private static final int DELTA = 10;

        public ControlPanel() {
            this.add(new MoveButton("\u2190", KeyEvent.VK_LEFT, -DELTA, 0));
            this.add(new MoveButton("\u2191", KeyEvent.VK_UP, 0, -DELTA));
            this.add(new MoveButton("\u2192", KeyEvent.VK_RIGHT, DELTA, 0));
            this.add(new MoveButton("\u2193", KeyEvent.VK_DOWN, 0, DELTA));
        }

        private class MoveButton extends JButton {

            KeyStroke k;
            int dx, dy;

            public MoveButton(String name, int code, final int dx, final int dy) {
                super(name);
                this.k = KeyStroke.getKeyStroke(code, 0);
                this.dx = dx;
                this.dy = dy;
                this.setAction(new AbstractAction(this.getText()) {

                    @Override
                    public void actionPerformed(ActionEvent e) {
                        LinePanel.this.p1.translate(dx, dy);
                        LinePanel.this.p2.translate(dx, dy);
                        LinePanel.this.repaint();
                    }
                });
                ControlPanel.this.getInputMap(
                    WHEN_IN_FOCUSED_WINDOW).put(k, k.toString());
                ControlPanel.this.getActionMap().put(k.toString(), new AbstractAction() {

                    @Override
                    public void actionPerformed(ActionEvent e) {
                        MoveButton.this.doClick();
                    }
                });
            }
        }
    }

    private void display() {
        JFrame f = new JFrame("LinePanel");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.add(this);
        f.add(new ControlPanel(), BorderLayout.SOUTH);
        f.pack();
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                new LinePanel().display();
            }
        });
    }
}