2

I am trying to use some sort of draw method to draw a sprite image to my subclass of JPanel called AnimationPanel. I have created a Spritesheet class which can generate a BufferedImage[] that contains all of the sprites in the sheet. In my AnimationPanel class, which implements Runnable, I am getting that BufferedImage[] from the spritesheet instantiated in the AnimationPanel constructor. I want to be able to loop through this array and display each sprite to the screen. How would I do this? Here are my AnimationPanel and Spritesheet classes.

AnimationPanel

package com.kahl.animation;

import javax.swing.JPanel;

public class AnimationPanel extends JPanel implements Runnable {

//Instance Variables
private Spritesheet sheet;
private int currentFrame;
private Thread animationThread;
private BufferedImage image;

public AnimationPanel(Spritesheet aSheet) {
    super();
    sheet = aSheet;
    setPreferredSize(new Dimension(128,128));
    setFocusable(true);
    requestFocus();

}

public void run() {
    BufferedImage[] frames = sheet.getAllSprites();
    currentFrame = 0;
    while (true) {
        frames[currentFrame].draw(); //some implementation still necessary here
        currentFrame++;
        if (currentFrame >= frames.length) {
            currentFrame = 0;
        }
    }
}

public void addNotify() {
    super.addNotify();
    if (animationThread == null) {
        animationThread = new Thread(this);
        animationThread.start();
    }
}

}

Spritesheet

package com.kahl.animation;

import java.awt.image.BufferedImage;
import java.imageio.ImageIO;
import java.io.IOException;
import java.io.File;

public class Spritesheet {

//Instance Variables
private String path;
private int frameWidth;
private int frameHeight;
private int framesPerRow;
private int frames;
private BufferedImage sheet = null;

//Constructors
public Spritesheet(String aPath,int width,int height,int fpr, int numOfFrames) {

    path = aPath;
    frameWidth = width;
    frameHeight = height;
    framesPerRow = fpr;
    frames = numOfFrames;

    try {
        sheet = ImageIO.read(getClass().getResourceAsStream());
    } catch (IOException e) {
        e.printStackTrace();
    }

}

//Methods

public int getHeight() {
    return frameWidth;
}

public int getWidth() {
    return frameWidth;
}

public int getFramesPerRow() {
    return framesPerRow;
}

private BufferedImage getSprite(int x, int y, int h, int w) {
    BufferedImage sprite = sheet.getSubimage(x,y,h,w);
}

public BufferedImage[] getAllSprites() {
    BufferedImage[] sprites = new BufferedImage[frames];
    int y = 0;
    for (int i = 0; i < frames; i++) {
        x = i * frameWidth;
        currentSprite = sheet.getSprite(x,y,frameHeight,frameWidth);
        sprites.add(currentSprite);
    }
    return sprites;

}

}
Andrew Thompson
  • 168,117
  • 40
  • 217
  • 433
redeagle47
  • 1,355
  • 4
  • 14
  • 26
  • 1) For better help sooner, post an [MCVE](http://stackoverflow.com/help/mcve) (Minimal Complete Verifiable Example) or [SSCCE](http://www.sscce.org/) (Short, Self Contained, Correct Example). Strictly speaking an MCVE can have only one source file and one **`public`** class. It also needs a `main(string[])` method to run it. 2) One way to get images for an example, is to hot link to images seen in [this Q&A](http://stackoverflow.com/q/19209650/418556). – Andrew Thompson Jan 13 '15 at 23:08
  • The only problem with that is I haven't even gotten it to run yet. The parts that would actually run are the parts I'm having trouble with. I will keep that in mind for the future, though. Thank you for the information. – redeagle47 Jan 13 '15 at 23:25

2 Answers2

10
  1. I'd encourage the use of a javax.swing.Timer to control the frame rate, rather than an uncontrolled loop
  2. Once the timer "ticks", you need to increment the current frame, get the current image to be rendered and call repaint on the JPanel
  3. Use Graphics#drawImage to render the image.

See...

for more details

There is a cascading series of issues with your Spritesheet class, apart from the fact that it won't actually compile, there are issues with you returning the wrong values from some methods and relying on values which are better calculated...

I had to modify your code so much, I can't remember most of them

public int getHeight() {
    return frameWidth;
}

and

public BufferedImage[] getAllSprites() {
    BufferedImage[] sprites = new BufferedImage[frames];
    int y = 0;
    for (int i = 0; i < frames; i++) {
        x = i * frameWidth;
        currentSprite = sheet.getSprite(x,y,frameHeight,frameWidth);
        sprites.add(currentSprite);
    }
    return sprites;

}

Stand out as two main examples...

Run

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class TestSpriteSheet {

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

    public TestSpriteSheet() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        private Spritesheet spritesheet;
        private BufferedImage currentFrame;
        private int frame;

        public TestPane() {
            spritesheet = new Spritesheet("/Sheet02.gif", 240, 220);
            Timer timer = new Timer(100, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    currentFrame = spritesheet.getSprite(frame % spritesheet.getFrameCount());
                    repaint();
                    frame++;
                }
            });
            timer.start();
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(240, 220);
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            if (currentFrame != null) {
                Graphics2D g2d = (Graphics2D) g.create();
                int x = (getWidth() - currentFrame.getWidth()) / 2;
                int y = (getHeight() - currentFrame.getHeight()) / 2;
                g2d.drawImage(currentFrame, x, y, this);
                g2d.dispose();
            }
        }

    }

    public class Spritesheet {

//Instance Variables
        private String path;
        private int frameWidth;
        private int frameHeight;
        private BufferedImage sheet = null;
        private BufferedImage[] frameImages;

//Constructors
        public Spritesheet(String aPath, int width, int height) {

            path = aPath;
            frameWidth = width;
            frameHeight = height;

            try {
                sheet = ImageIO.read(getClass().getResourceAsStream(path));
                frameImages = getAllSprites();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }

        public BufferedImage getSprite(int frame) {
            return frameImages[frame];
        }

//Methods
        public int getHeight() {
            return frameHeight;
        }

        public int getWidth() {
            return frameWidth;
        }

        public int getColumnCount() {
            return sheet.getWidth() / getWidth();
        }

        public int getRowCount() {
            return sheet.getHeight() / getHeight();
        }

        public int getFrameCount() {
            int cols = getColumnCount();
            int rows = getRowCount();
            return cols * rows;
        }

        private BufferedImage getSprite(int x, int y, int h, int w) {
            BufferedImage sprite = sheet.getSubimage(x, y, h, w);
            return sprite;
        }

        public BufferedImage[] getAllSprites() {
            int cols = getColumnCount();
            int rows = getRowCount();
            int frameCount =  getFrameCount();
            BufferedImage[] sprites = new BufferedImage[frameCount];
            int index = 0;
            System.out.println("cols = " + cols);
            System.out.println("rows = " + rows);
            System.out.println("frameCount = " + frameCount);
            for (int row = 0; row < getRowCount(); row++) {
                for (int col = 0; col < getColumnCount(); col++) {
                    int x = col * getWidth();
                    int y = row * getHeight();
                    System.out.println(index + " " + x + "x" + y);
                    BufferedImage currentSprite = getSprite(x, y, getWidth(), getHeight());
                    sprites[index] = currentSprite;
                    index++;
                }
            }
            return sprites;

        }

    }
}

Remember, animation is the illusion of change over time. You need to provide a delay between each frame of the animation, long enough for the user to recognise it, but short enough to make the animation look smooth.

In the above example, I've used 100 milliseconds, simply as an arbitrary value. It could be possible to use something more like 1000 / spritesheet.getFrameCount(), which will allow a full second for the entire animation (all the frames within one second).

You might need to use different values, for longer or short animations, depending on your needs

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • thank you for taking the time to write all of that up. I've never really done anything like this before, so I'm not too surprised there were some things wrong with my code. – redeagle47 Jan 14 '15 at 00:11
  • Remember, animation is the illusion of change over time. Apart from a few other things, your code is missing the "time". The example above uses roughly 10fps (100 millisecond delay between each from), in fact, given that there is only 6 frames of animation, we might be able to use something more like 166, depending on how many cycles of the animation you want per second... – MadProgrammer Jan 14 '15 at 00:18
  • where in your code does the previous image get taken off the screen? After working with your code for a bit, I was able to get sprites on screen, but the animation just lays them over top of each other. – redeagle47 Jan 14 '15 at 04:49
  • Forgot to call `super.paintComponent`? – MadProgrammer Jan 14 '15 at 04:55
  • sorry, i was going to comment back last night, yeah that's what it was. – redeagle47 Jan 14 '15 at 21:54
2

Here's some generic code for drawing an image to a JPanel. This method is called to paint your JPanel component.

public void paintComponent (Graphics g)
{ 
     super.paintComponent(g);
     //I would have image be a class variable that gets updated in your run() method
     g.drawImage(image, 0, 0, this); 
} 

I may also modify run() to look something like this:

public void run() {
  BufferedImage[] frames = sheet.getAllSprites();
  currentFrame = 0;
  while (true) {
    image = frames[currentFrame];
    this.repaint(); //explicitly added "this" for clarity, not necessary.
    currentFrame++;
    if (currentFrame >= frames.length) {
        currentFrame = 0;
    }
  }
}

In regards to only repainting part of the component, it gets a little more complicated

public void run() {
  BufferedImage[] frames = sheet.getAllSprites();
  currentFrame = 0;
  while (true) {
    image = frames[currentFrame];
    Rectangle r = this.getDirtyRect();
    this.repaint(r); 
    currentFrame++;
    if (currentFrame >= frames.length) {
        currentFrame = 0;
    }
  }
}

public Rectangle getDirtyRect() {
  int minX=0; //calculate smallest x value affected
  int maxX=0; //calculate largest x value affected
  int minY=0; //calculate smallest y value affected
  int maxY=0; //calculate largest y value affected 
  return new Rectangle(minX,minY,maxX,maxY);
}
Matt
  • 5,404
  • 3
  • 27
  • 39
  • @MadProgrammer, This might be beyond my knowledge. What do you mean by that? – Matt Jan 13 '15 at 23:12
  • You've failed to honour the requirements of the super class and broken the paint method call chain. The example could result in unexpected and undesirable paint glitches, see [Painting in AWT and Swing](http://www.oracle.com/technetwork/java/painting-140037.html) and [Performing Custom Painting](http://docs.oracle.com/javase/tutorial/uiswing/painting/) for more details – MadProgrammer Jan 13 '15 at 23:13
  • I think he means you need to call super.paintComponent(g); first – redeagle47 Jan 13 '15 at 23:14
  • oh yeah. Sorry was going quickly from memory, I agree. – Matt Jan 13 '15 at 23:14
  • okay, better or no? I agree that I needed to call the super's paintComponent method. – Matt Jan 13 '15 at 23:16
  • when doing it this way, how does g.drawImage get the image? I feel like this is a stupid question, but I've been working on this for the past few hours. Are you saying that I don't have pass in the image I'm trying to draw, i.e. it can take it from context? Also, what if I don't want to repaint the whole component and only wanted to repaint certain parts? – redeagle47 Jan 13 '15 at 23:17
  • 1
    well you have an instance variable saved in your class as "private BufferedImage image;" Ideally your run method would update the value of this and then call "repaint();" I'll add a bit about this to the answer. – Matt Jan 13 '15 at 23:19
  • okay @Matt that makes sense. Anything to say about the second part of my question (there at the end of my last comment) ? – redeagle47 Jan 13 '15 at 23:23
  • If you know the bounding rectangle that has been changed within the component, you can pass a rectangle with those bounds to the repaint method. Check out "http://www.java2s.com/Code/Java/2D-Graphics-GUI/repaintjusttheaffectedpartofthecomponent.htm" – Matt Jan 13 '15 at 23:27
  • My solution with the dirty rectangle uses Graphics2D, which may not be an option for you. – Matt Jan 13 '15 at 23:36