Search code examples
javaanimationpaintcomponent

Make animation more smoother


I’m trying to create some animation ( boy who runs whenever key is pressed) using JPanel and painComponent. So, first of all I declare some images, Image array and some methods for drawing. I created timer and added it to constructor.

Image img1;
Image img2;
Image img3;
Image img4;

int index = 0;

Image currentImage l;
Image[] images = new Image[4]

public class Animation extends JPanel implements ActionListener
{

    public Animation()
    {
     loadImages();
     getTimer();
     imgAdder();
     addFirstImage();
    }

    public void serCurrentImage(Image currentImage)
  {
     this.currentImage = currentImage;
   }
     public void getTimer()
   {
     Timer timer = new Timer(20,this)
      timer.start();
   }
     public void imgAdder()
   {
     images[1] = img1;
        ...
   }
     public void addFirstImage()
   {
      currentImage = img1;
   }
      private void loadImages()
   {
       try
   {
      BufferedImage img = ImageIO.read(getClass.getResource(“/resources/images/img1.png”);
      img1 = img;
      // and so on for every image
    }catch(IOexception ioe )
     ioe.printStackTrace();
    }
  }
     public void paintComponent (Graphics g)
     {
           super.paintComponent(g);
           g.drewImage(currentImage,0,0,this);
           requestsFocus();
      }
            public class FieldKeyListener extends KeyAdapter
    {
            public void move()
      {
            setCurrentImage(image[index]);
             index++;
             if( index == 4 )
                      index = 0;
       }
             public void keyPressed(KeyEvent e)
       {
             super.keyPressed(e);
             int key = e.getKeyCode();
             if(key == Key.Event.VK_LEFT)
             move();
       }
     }
}

Then drew all images through paintComponent using loop for my array. Also I declared class which extends KeyAdapter. Everything seems to be fine and my animation works, but problem is that it works not as smoothly as I wanted. When I press and hold key, images are changing too fast and process look unnatural. I want , for instance , 3 or 4 images change per second instead of 20. May I added timer in wrong method ? May be there is something like time delay. I don’t know how exactly it works , and which listener should I mention as argument in timer. P.s. I’m just beginner and my code may look incorrectly in terms of coding standards. Also I wrote just crucial parts of my project which represent problem. I hope you help me with this. Thanks in advance.


Solution

  • Animation is a complex subject, with lots of boring theory. Basically, animation is the illusion of change over time. This is very important, as everything you do in animation will based around time.

    In something like a game, you will have a bunch of entities all playing at a different rates of time. One of the challenges is taking the time to devise a solution which allows a entity to play over a period of time while been decoupled from the refresh cycle (ie frame count), unless you have sprite with the correct number of frames to match you refresh cycle, but even then, I'd be concerned, as the system won't be flexible enough to adapt to situations where the OS and hardware can't keep up.

    The following is a simple example which takes a sprite sheet (a series of images stored in a single image), the number of expected images/frames and the time to complete a full cycle.

    It calculates the individual frame size and returns a frame based on the amount of time that the sprite has been animated...

    public class Sprite {
    
        private BufferedImage source;
        private int imageCount;
        private int imageWidth;
    
        // How long it takes to play a full cycle
        private Duration duration;
        // When the last cycle was started
        private Instant startedAt;
    
        public Sprite(BufferedImage source, int imageCount, int cycleTimeInSeconds) throws IOException {
            this.source = source;
            this.imageCount = imageCount;
            imageWidth = source.getWidth() / imageCount;
            duration = Duration.ofSeconds(cycleTimeInSeconds);
        }
    
        public BufferedImage getFrame() {
            if (startedAt == null) {
                startedAt = Instant.now();
            }
            Duration timePlayed = Duration.between(startedAt, Instant.now());
            double progress = timePlayed.toMillis() / (double)duration.toMillis();
            if (progress > 1.0) {
                progress = 1.0;
                startedAt = Instant.now();
            }
            int frame = Math.min((int)(imageCount * progress), imageCount - 1);
            return getImageAt(frame);
        }
    
        protected BufferedImage getImageAt(int index) {
            if (index < 0 || index >= imageCount) {
                return null;
            }
            int xOffset = imageWidth * index;
            return source.getSubimage(xOffset, 0, imageWidth, source.getHeight());
        }
    
    }
    

    nb: It also needs a means to be reset or stopped, so you can force the sprite back to the start, but I'll leave that to you

    Next, we need some way to play the animation

    public class TestPane extends JPanel {
    
        private Sprite sprite;
    
        public TestPane(Sprite sprite) {
            this.sprite = sprite;
            Timer timer = new Timer(5, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    repaint();
                }
            });
            timer.start();
        }
    
        @Override
        public Dimension getPreferredSize() {
            return new Dimension(400, 400);
        }
    
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();
            BufferedImage img = sprite.getFrame();
            int x = (getWidth() - img.getWidth()) / 2;
            int y = (getHeight() - img.getHeight()) / 2;
            g2d.drawImage(img, x, y, this);
            g2d.dispose();
        }
    
    }
    

    There's nothing really special here, it's a simple Swing Timer set to a high resolution (5 milliseconds) which constantly updates the UI, requesting the next frame from the sprite and painting it.

    The important part here is the sprite and the refresh cycle are independent. Want the character to walk faster, change the sprite duration, want the character walk slower, changed the sprite duration, the refresh cycle doesn't need be altered (or any other entity)

    So, starting with...

    Sprite Sheet

    Same cycle, first over 1 second, second over 5 seconds

    FastwalkSlowWalk

    You can also have a look at something like How to create a usable KeyReleased method in java, which demonstrates the use of key bindings and a centralised Set as a "action" repository