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.
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...
Same cycle, first over 1 second, second over 5 seconds
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
Related
I have a class for the game where most of the rendering and framework is done. I have a class for the mouse listener. I also have a class called Menu that draws a menu on the canvas. I want it to actually start the game when I click on the "Start" button but it seems as though the MouseListener is not receiving the mouse click.
I have tried putting the line addMouseListener(new MouseInput()) in many places throughout the Game class but it will not work.
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
public class MouseInput implements MouseListener
{
public void mousePressed(MouseEvent e)
{
int mx = e.getX();
int my = e.getY();
if(Game.STATE == 0)
{
if(mx >= 415 && mx <= 615)
{
if(my >= 350 && my <= 425)
{
Game.STATE = Game.STATE + 1;
}
}
if(mx >= 415 && mx <=615)
{
if(my >= 500 && my <= 575)
{
System.exit(1);
}
}
}
}
}
//Game Class
public class Game extends JFrame implements Runnable
{
private Canvas c = new Canvas();
public static int STATE = 0;
public static final int WIDTH = 1000;
public static final int HEIGHT = 800;
private Menu menu;
private FightState fight;
public Game()
{
//Forces program to close when panel is closed
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//sets position and size of Frame
setBounds(0, 0, WIDTH, HEIGHT);
//puts Frame in center of the screen
setLocationRelativeTo(null);
//adds canvas to game
add(c);
//Makes frame visible
setVisible(true);
//creates our object for buffer strategy
c.createBufferStrategy(2);
// adds the mouse listner;
addMouseListener(new MouseInput());
}
public void update()
{
}
//renders the graphics onto the screen
public void render()
{
BufferStrategy bufferStrategy = c.getBufferStrategy();
Graphics g = bufferStrategy.getDrawGraphics();
super.paint(g);
//instantiates the menu object
menu = new Menu();
//instantiates the FightState object
fight = new FightState();
//renders the menu
if(STATE == 0)
{
menu.render(g);
}
//renders the fight stage
if(STATE == 1)
{
fight.render(g);
}
g.setFont(new Font("Monospaced", Font.PLAIN, 35));
g.drawString("STATE: " + STATE, 10, 400);
repaint();
//checks if mouseListener is working
System.out.print(STATE);
g.dispose();
bufferStrategy.show();
}
//game loop
public void run()
{
BufferStrategy bufferStrategy = c.getBufferStrategy();
long lastTime = System.nanoTime(); //long is an int that stores more space
double nanoSecondConvert = 1000000000.0 / 60; //frames/sec
double deltaSeconds = 0;
while(true)
{
long now = System.nanoTime();
deltaSeconds += (now-lastTime)/nanoSecondConvert;
while(deltaSeconds >=1)
{
update();
deltaSeconds = 0;
}
render();
lastTime = now;
System.out.println("STATE: " + STATE);
}
}
//main method
public static void main(String[] args)
{
Game game = new Game();
Thread gameThread = new Thread(game);
gameThread.start();
}
}
Don't call super.paint(g); on the JFrame if you're using a BufferStrategy. Just paint to the buffer directly. You should also be adding your MouseListener to the Canvas, not the frame.
The Canvas is laid out WITHIN the frame boundaries of the window, meaning it will be offset and smaller than the actual frame itself.
Mouse events are automatically converted to the sources coordinate context, this means, you are currently trying to compare values coming from the frame's coordinate context with values been used by the Canvas which are different
One question: How would i paint directly to the buffer if buffer doesnt have the "Graphics methods" such as fillRect()?
Graphics g = bufferStrategy.getDrawGraphics() gives you the Graphics context, you then paint directly to it.
You don’t want to call paint directly (ever) as it can be called by the system and you could up with race conditions and other issues
Swing uses a different painting algorithm, which you've opted out of by using a BufferStrategy, this means you can no longer make use of the "normal" Swing painting process and instead, must write your own
I have looked into Double Buffering and plan on implementing it eventually but as of right now I can't figure out how to use it or anything like it. I am trying to make pong so I plan on adding three objects total but for now I just want to get one object to work smoothly. I'm fairly new to graphics so I don't know entirely what I'm doing and I'm just trying to learn as I go.
Here is my code:
Pong:
public static void main(String[]args) {
JFrame window= new JFrame();
window.setTitle("Pong Game");
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
window.setPreferredSize(new Dimension(800,500));
window.pack();
window.setVisible(true);
Ball ball= new Ball();
Paddle player= new Paddle();
window.getContentPane().add(ball);
for(;;) {
ball.move();
//window.setContentPane(ball);
window.setContentPane(player);
player.move();
}
}
Paddles:
double x, y, ymove;
boolean cpu;
public Paddle() {
x=5;
y=180;
ymove=.1;
}
//passing an integer through to make the computer paddle
public Paddle(int a) {
cpu= true;
x=761;
y=180;
ymove=.1;
}
public void paint(Graphics g) {
g.setColor(Color.blue);
g.fillRect((int)x, (int)y, 18, 120);
}
public void move() {
y+=ymove;
if(y>=500-160||y<=0) {
ymove*=-1;
}
}
Ball:
double x, y, xspeed, yspeed;
public Ball() {
x=200;
y=200;
xspeed=0;
yspeed=.1;
}
public void move() {
x+=xspeed;
y+=yspeed;
if(y>=440||y<=0) {
yspeed*=-1;
}
}
public void paint(Graphics g) {
g.setColor(Color.black);
g.fillOval((int)x, (int)y, 20, 20);
}
This Answer is a very very simplified explanation! I recommend checking out this linux journal, which explores something similia.
The main issue, which causes the "flickering" is, that the draw is done "to fast".
Take your main loop:
for(;;) {
ball.move();
window.setContentPane(ball);
window.setContentPane(player);
player.move();
}
This loop updates the positions of the ball and afterwards "adds it to the content pane". While it is drawn, the next image is already added and drawn. This is causing the flickering (again, note: this is very simplified).
The simplest solution to fix the "flickering" is, to let the Thread sleep after it has drawn and "wait" until the draw is finished.
boolean running = true;
int delay = 15; // adjust the delay
while(running) {
ball.move();
player.move();
window.setContentPane(ball);
window.setContentPane(player);
try {
Thread.sleep(delay);
} catch(InterruptedException e) {
// We were interrupted while waiting
// Something "woke us up". Stop the loop.
e.printStackTrace();
running = false;
}
}
This Thread.sleep method let's the current Thread "wait" for the specified time.
The delay can be adjusted to something more practical. You could for example calculate how many frames you want and sleep for that amount.
Another way would be to "time" the updates. This could be done with a timer. Since it is more or less deprecated, i implement it using the ScheduledExecutorService
int delay = 15; // adjust the delay
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool();
scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
public void run() {
ball.move();
player.move();
}
}, delay, TimeUnit.MILLISECONDS)
As of Java8, you might write this using a Lambda like this:
scheduledExecutorService.scheduleAtFixedRate(() -> {
ball.move();
player.move();
}, delay, TimeUnit.MILLISECONDS)
To stop it, you could now call:
scheduledExecutorService.shutdown();
However: There are more sophisticated solutions. One is, as you already noted, the double buffering. But there are also multiple different techniques, that compensate more difficult problems. They use something called page flipping.
The problem you are having is that you have split your paint methods,
if you make one class that is dedicated to doing the painting and you put all of your paints in one paint method it should work without flickering.
I would also recommend looking into making your paint calls run on a timer which lets you decide the refresh rate which usually leads to a smoother experience overall.
Here is an example of my graphics class in my latest game,
class GameGraphics extends JPanel implements ActionListener {
private Timer refreshHZTimer;
private int refreshHZ = 10;
private int frameID = 0;
public GameGraphics(int width, int height) {
setBounds(0,0, width, height);
setVisible(true);
refreshHZTimer = new Timer(refreshHZ, this);
refreshHZTimer.start();
}
#Override
public void actionPerformed(ActionEvent e) {
frameID++;
if (frameID % 100 == 1)
System.out.println("Painting FrameID: " + frameID);
repaint();
}
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
//Init graphics
Graphics2D g2 = (Graphics2D) g;
//Paint stuff here:
}
}
And add this code to your JFrame constructor:
GameGraphics gpu = new GameGraphics(width, height);
frame.add(gpu);
I'm building my first very basic game in JAVA without using any other external libraries.
The problem I got is
You can see that the background is rendered over player how can I fix that
The player rendering code is as follows
#Override
public void render(Graphics g) {
int bulletcount = 0;
//render player
g.setColor(color);
g.fillRect(x,y,32,32);
if(shooting) {
handler.add(new bullet(x + 8, y - 24, ID.bullet, handler));
bulletcount++;
}
}
and the background render code is as follows.
public class background extends GameObject{
private int width;
private Random random = new Random();
public background(int x, int y , ID id,Handler handler){
super(x,y,id,handler);
valy = 2;
width = getWidth(x,800);
}
public void tick() {
y += valy;
if(y>650)
handler.remove(this);
}
public void render(Graphics g) {
g.setColor(Color.white);
g.fillRoundRect(x,y,width,10,1,1);
}
public int getWidth(int x,int width1){
int wid;
while(true){
if((width - x) > 35) {
wid = random.nextInt((width-x-35));
break;
}
else{
x -= 10;
}
}
return wid;
}
}
the main class of game can see the order of flow of execution.
private void render() {
// render the Game
BufferStrategy bs = this.getBufferStrategy();
if(bs == null)
{
this.createBufferStrategy(3);
return;
}
Graphics g = bs.getDrawGraphics();
g.setColor(Color.black);
g.fillRect(0,0,WIDTH,HEIGHT);
handler.render(g);
hod.render(g); //handler is rendering every object in game
g.dispose();
bs.show();
}
private void tick() {
//update Game
handler.tick();
hod.tick();
spawner.tick(); // background is generating in spawner class.
}
I know that telling the problem with this much less code is hard for you all. But I have complete faith in this large and powerful community.
Thanks in advance. Don't hesitate to edit and point out my mistakes Thanks!
If I get the code right, the background is being generated after rendering the player which leads to the background being on top of the player. I would try calling spawner.tick() (where I guess the background is coming from given your code comment) before calling hod.tick(). Hopefully that should resolve this issue.
private void tick() { //update Game
spawner.tick();
handler.tick();
hod.tick();
}
I just had solution!
The problem was in handler.render(g); method:- which is a follows;
private void render(Graphics g){
for(int i =0; i<gameobj.size(); i++){//gameobj is linkedlist of GameObject
GameObject tempobj = gameobj.get(i);
tempobj.render(g);//getting every object and rendering them!
}
The problem was in loop the first object added in the game is player.So , it was getting render first after that the background was getting rendered.
It was a silly mistake.which got right by following change.->
private void render(Graphics g){
for(int i = gameobj.size()-1; i > 0; i--){//gameobj is linkedlist of GameObject
GameObject tempobj = gameobj.get(i);
tempobj.render(g);//getting every object and rendering them! from reverse
}
so, the last object renders first and the first object renders last.
Peace!
I'm trying to animate the sprite in my game when a button is pressed, but when I press the button, it skips the animation. Its supposed to go one pixel, change sprites, and then go one more pixel and change back. Here is the code
//for all
import java.nio.file.*;
import javax.imageio.ImageIO;
import java.io.IOException;
import java.awt.image.*;
import java.net.*;
import java.awt.*;
import javax.swing.*;
import static java.lang.invoke.MethodHandles.*;
import java.awt.event.*;
//my Mario class (cut down a lot)
class Mario {
// all numbers multiplied by 2 from OG game
protected Direction dir;
protected int x, y;
protected BufferedImage sprite;
protected String currentSpriteName;
public Mario() {
this.x = 54;
this.y = 808;
dir = Direction.RIGHT;
setSprite(MVCE.SMALLSTANDFACERIGHT);
currentSpriteName = MVCE.SMALLSTANDFACERIGHT;
}
public void moveRight(){
if(this.dir == Direction.LEFT){
this.dir = Direction.RIGHT;
}
else if(this.dir == Direction.RIGHT){
this.x+=1;
}
}
public void animateMoveRight(){
if (currentSpriteName.equals(MVCE.SMALLSTANDFACERIGHT)){
setSprite(MVCE.SMALLWALKFACERIGHT);
}
else if (currentSpriteName.equals(MVCE.SMALLWALKFACERIGHT)){
setSprite(MVCE.SMALLSTANDFACERIGHT);
}
}
public void jump() {
this.y -= 46;
}
public void setSprite(String spriteName) {
URL spriteAtLoc = MVCE.urlGenerator(spriteName);
this.sprite = MVCE.generateAndFilter(sprite, spriteAtLoc);
}
public void getSprite(){
System.out.println(this.currentSpriteName);
}
public void paint(Graphics g) {
Graphics2D g2 = (Graphics2D) g;
g2.drawImage(sprite, 0, 0, null); // DO NOT SET x and y TO ANYTHING,
// this sets 0,0 to top left!!
}
}
// my MarioRender class:
class MarioRender extends JLabel {
protected Mario marioSprite;
public MarioRender() {
marioSprite = new Mario();
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
marioSprite.paint(g2);
setBounds(marioSprite.x, marioSprite.y, marioSprite.sprite.getWidth(), marioSprite.sprite.getHeight());
}
public void moveMarioRight(){
marioSprite.moveRight();
marioSprite.animateMoveRight();
setLocation(this.marioSprite.getX(), this.marioSprite.getY());
repaint();
//this is my attempt to make it animate
marioSprite.moveRight();
marioSprite.animateMoveRight();
setLocation(this.marioSprite.getX(), this.marioSprite.getY());
repaint();
}
public void jumpMario() {
marioSprite.jump();
setLocation(this.marioSprite.x, this.marioSprite.y);
repaint();
}
}
// direction class, solely for moving
enum Direction {
LEFT, RIGHT
}
// my calling class, which I called MVCE where I make the frame
public class MVCE extends JFrame {
MarioRender m = new MarioRender();
JLabel bg;
public MVCE() {
bg = new JLabel();
this.setSize(868, 915);
this.setVisible(true);
this.add(bg, BorderLayout.CENTER);
bg.setLayout(null);
bg.add(m);
m.setBounds(m.marioSprite.x, m.marioSprite.y, m.marioSprite.sprite.getWidth(),
m.marioSprite.sprite.getHeight());
KeyListener kl = new MoveListener();
this.addKeyListener(kl);
this.setFocusable(true);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
public static final String SMALLSTANDFACERIGHT = "SmallStandFaceRight.bmp"; // 30
// x
// 32
public static final String SMALLJUMPFACERIGHT = "SmallJumpFaceRight.bmp"; // 32
// x
// 32
// generate URL
public static URL urlGenerator(String name) {
URL u = lookup().lookupClass().getResource(name);
return u;
}
// return image with filtered color
public static BufferedImage generateAndFilter(BufferedImage b, URL u) {
try {
b = ImageIO.read(u);
int width = b.getWidth();
int height = b.getHeight();
int[] pixels = new int[width * height];
b.getRGB(0, 0, width, height, pixels, 0, width);
for (int i = 0; i < pixels.length; i++) {
// System.out.println(pixels[i]);
if (pixels[i] == 0xFFff00fe) {
pixels[i] = 0x00ff00fe;
}
}
BufferedImage newSprite = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
newSprite.setRGB(0, 0, width, height, pixels, 0, width);
b = newSprite;
} catch (IOException e) {
System.out.println("sprite not found");
e.printStackTrace();
}
return b;
}
// key listener
class MoveListener implements KeyListener {
public void keyPressed(KeyEvent k) {
if ((k.getKeyCode() == 39)) {
m.moveMarioRight();
///THIS IS SUPPOSED TO MOVE HIM 1, change sprite, and automatically move him back, it moves 2 pixels but no animation
}
if (k.getKeyCode() == 83) { // S key
m.marioSprite.setSprite(SMALLJUMPFACERIGHT);
m.jumpMario();
}
}
public void keyReleased(KeyEvent k) {
}
public void keyTyped(KeyEvent k) {
}
}
public static void main(String[] args) {
MVCE m = new MVCE();
}
}
I tried putting this between the calls to marioMoveRight():
try {
Thread.sleep(200);
} catch(InterruptedException ex) {
Thread.currentThread().interrupt();
}
but it just delays the whole thing. I had also tried using an ActionListener, but I don't know how to make it react only when the key is pushed. as I had it,
I had this class inside of MVCE:
class TickListener implements ActionListener{
public void actionPerformed(ActionEvent a){
m.marioSprite.setSprite(Constants.SMALLWALKFACERIGHT);
repaint();
}
}
and this at the end of the MVCE constructor:
ActionListener ac = new TickListener();
final int DELAY = 1000;
Timer t = new Timer(DELAY, ac);
t.start();
but then, the Mario just moves automatically. I do not want to use a sprite sheet for this project, I am trying to do it as this guy did for SMB1.
Many problems, don't know which one or if any will fix the problem:
Don't use a KeyListener. If a component doesn't have focus the component won't receive the event. Instead use Key Bindings.
Don't use "==" to compare Objects. Instead you should be using the equals(...) method.
Don't override paintComponent. A painting method is for painting only. You should not be changing the bounds of the component in the painting method.
Do basic debugging (problem solving) before asking a question. A simple System.out.println(...) added to various methods will determine if the code is executing as you expect. Then when you ask a question you can ask a specific question telling us which block of code does not execute as you expect.
You never actually call the method animateMoveRight(), and if I understand correcly, that's what's changing the sprite. Also, I doubt that you see the sprite change when calling the same method twice in a row without any delay.
Try putting the animateMoveRight() method into the moveRight() or the moveMarioRight() method and, if neccessary because the animation is too fast, add your delay code back where you had it. Be careful not to let the main thread sleep, as this causes everything to freeze, so start another one or use a timer etc.
EDIT: Good timers
I'm not too familiar with the Timer class, so I end up using the Thread variant. There are many tutorials for that out there, just search for "java threads" or "java multithreading". This is IMO a solid tutorial you can check out.
I'm creating a graphical front-end for a JBox2D simulation. The simulation runs incrementally, and in between the updates, the contents of the simulation are supposed to be drawn. Similar to a game except without input.
I only need geometric primitives to draw a JBox2D simulation. This API seemed like the simplest choice, but its design is a bit confusing.
Currently I have one class called Window extending JFrame, that contains as a member another class called Renderer. The Window class only initializes itself and provides an updateDisplay() method (that is called by the main loop), that calls updateDisplay(objects) method on the Renderer. I made these two methods myself and their only purpose is to call repaint() on the Renderer.
Is the JPanel supposed to be used that way? Or am I supposed to use some more sophisticated method for animation (such that involves events and/or time intervals in some back-end thread)?
If you are wanting to schedule the updates at a set interval, javax.swing.Timer provides a Swing-integrated service for it. Timer runs its task on the EDT periodically, without having an explicit loop. (An explicit loop would block the EDT from processing events, which would freeze the UI. I explained this more in-depth here.)
Ultimately doing any kind of painting in Swing you'll still be doing two things:
Overriding paintComponent to do your drawing.
Calling repaint as-needed to request that your drawing be made visible. (Swing normally only repaints when it's needed, for example when some other program's window passes over top of a Swing component.)
If you're doing those two things you're probably doing it right. Swing doesn't really have a high-level API for animation. It's designed primarily with drawing GUI components in mind. It can certainly do some good stuff, but you will have to write a component mostly from scratch, like you're doing.
Painting in AWT and Swing covers some of the 'behind the scenes' stuff if you do not have it bookmarked.
You might look in to JavaFX. I don't know that much about it personally, but it's supposed to be more geared towards animation.
As somewhat of an optimization, one thing that can be done is to paint on a separate image and then paint the image on to the panel in paintComponent. This is especially useful if the painting is long: repaints can be scheduled by the system so this keeps when it happens more under control.
If you aren't drawing to an image, then you'd need to build a model with objects, and paint all of them every time inside paintComponent.
Here's an example of drawing to an image:
import javax.swing.*;
import java.awt.*;
import java.awt.image.*;
import java.awt.event.*;
/**
* Holding left-click draws, and
* right-clicking cycles the color.
*/
class PaintAnyTime {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
new PaintAnyTime();
}
});
}
Color[] colors = {Color.red, Color.blue, Color.black};
int currentColor = 0;
BufferedImage img = new BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB);
Graphics2D imgG2 = img.createGraphics();
JFrame frame = new JFrame("Paint Any Time");
JPanel panel = new JPanel() {
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
// Creating a copy of the Graphics
// so any reconfiguration we do on
// it doesn't interfere with what
// Swing is doing.
Graphics2D g2 = (Graphics2D) g.create();
// Drawing the image.
int w = img.getWidth();
int h = img.getHeight();
g2.drawImage(img, 0, 0, w, h, null);
// Drawing a swatch.
Color color = colors[currentColor];
g2.setColor(color);
g2.fillRect(0, 0, 16, 16);
g2.setColor(Color.black);
g2.drawRect(-1, -1, 17, 17);
// At the end, we dispose the
// Graphics copy we've created
g2.dispose();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(img.getWidth(), img.getHeight());
}
};
MouseAdapter drawer = new MouseAdapter() {
boolean rButtonDown;
Point prev;
#Override
public void mousePressed(MouseEvent e) {
if (SwingUtilities.isLeftMouseButton(e)) {
prev = e.getPoint();
}
if (SwingUtilities.isRightMouseButton(e) && !rButtonDown) {
// (This just behaves a little better
// than using the mouseClicked event.)
rButtonDown = true;
currentColor = (currentColor + 1) % colors.length;
panel.repaint();
}
}
#Override
public void mouseDragged(MouseEvent e) {
if (prev != null) {
Point next = e.getPoint();
Color color = colors[currentColor];
// We can safely paint to the
// image any time we want to.
imgG2.setColor(color);
imgG2.drawLine(prev.x, prev.y, next.x, next.y);
// We just need to repaint the
// panel to make sure the
// changes are visible
// immediately.
panel.repaint();
prev = next;
}
}
#Override
public void mouseReleased(MouseEvent e) {
if (SwingUtilities.isLeftMouseButton(e)) {
prev = null;
}
if (SwingUtilities.isRightMouseButton(e)) {
rButtonDown = false;
}
}
};
PaintAnyTime() {
// RenderingHints let you specify
// options such as antialiasing.
imgG2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
imgG2.setStroke(new BasicStroke(3));
//
panel.setBackground(Color.white);
panel.addMouseListener(drawer);
panel.addMouseMotionListener(drawer);
Cursor cursor =
Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR);
panel.setCursor(cursor);
frame.setContentPane(panel);
frame.pack();
frame.setResizable(false);
frame.setLocationRelativeTo(null);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
If the routine is long-running and repaints could happen concurrently, double buffering can also be used. Drawing is done to an image which is separate from the one being shown. Then, when the drawing routine is done, the image references are swapped so the update is seamless.
You should typically use double buffering for a game, for example. Double buffering prevents the image from being shown in a partial state. This could happen if, for example, you were using a background thread for the game loop (instead of a Timer) and a repaint happened the game was doing the painting. Without double buffering, this kind of situation would result in flickering or tearing.
Swing components are double buffered by default, so if all of your drawing is happening on the EDT you don't need to write double buffering logic yourself. Swing already does it.
Here is a somewhat more complicated example which shows a long-running task and a buffer swap:
import java.awt.*;
import javax.swing.*;
import java.awt.image.*;
import java.awt.event.*;
import java.util.*;
/**
* Left-click to spawn a new background
* painting task.
*/
class DoubleBuffer {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
new DoubleBuffer();
}
});
}
final int width = 640;
final int height = 480;
BufferedImage createCompatibleImage() {
GraphicsConfiguration gc =
GraphicsEnvironment
.getLocalGraphicsEnvironment()
.getDefaultScreenDevice()
.getDefaultConfiguration();
// createCompatibleImage creates an image that is
// optimized for the display device.
// See http://docs.oracle.com/javase/8/docs/api/java/awt/GraphicsConfiguration.html#createCompatibleImage-int-int-int-
return gc.createCompatibleImage(width, height, Transparency.TRANSLUCENT);
}
// The front image is the one which is
// displayed in the panel.
BufferedImage front = createCompatibleImage();
// The back image is the one that gets
// painted to.
BufferedImage back = createCompatibleImage();
boolean isPainting = false;
final JFrame frame = new JFrame("Double Buffer");
final JPanel panel = new JPanel() {
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
// Scaling the image to fit the panel.
Dimension actualSize = getSize();
int w = actualSize.width;
int h = actualSize.height;
g.drawImage(front, 0, 0, w, h, null);
}
};
final MouseAdapter onClick = new MouseAdapter() {
#Override
public void mousePressed(MouseEvent e) {
if (!isPainting) {
isPainting = true;
new PaintTask(e.getPoint()).execute();
}
}
};
DoubleBuffer() {
panel.setPreferredSize(new Dimension(width, height));
panel.setBackground(Color.WHITE);
panel.addMouseListener(onClick);
frame.setContentPane(panel);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
void swap() {
BufferedImage temp = front;
front = back;
back = temp;
}
class PaintTask extends SwingWorker<Void, Void> {
final Point pt;
PaintTask(Point pt) {
this.pt = pt;
}
#Override
public Void doInBackground() {
Random rand = new Random();
synchronized(DoubleBuffer.this) {
Graphics2D g2 = back.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
RenderingHints.VALUE_STROKE_PURE);
g2.setBackground(new Color(0, true));
g2.clearRect(0, 0, width, height);
// (This computes pow(2, rand.nextInt(3) + 7).)
int depth = 1 << ( rand.nextInt(3) + 7 );
float hue = rand.nextInt(depth);
int radius = 1;
int c;
// This loop just draws concentric circles,
// starting from the inside and extending
// outwards until it hits the outside of
// the image.
do {
int rgb = Color.HSBtoRGB(hue / depth, 1, 1);
g2.setColor(new Color(rgb));
int x = pt.x - radius;
int y = pt.y - radius;
int d = radius * 2;
g2.drawOval(x, y, d, d);
++radius;
++hue;
c = (int) (radius * Math.cos(Math.PI / 4));
} while (
(0 <= pt.x - c) || (pt.x + c < width)
|| (0 <= pt.y - c) || (pt.y + c < height)
);
g2.dispose();
back.flush();
return (Void) null;
}
}
#Override
public void done() {
// done() is completed on the EDT,
// so for this small program, this
// is the only place where synchronization
// is necessary.
// paintComponent will see the swap
// happen the next time it is called.
synchronized(DoubleBuffer.this) {
swap();
}
isPainting = false;
panel.repaint();
}
}
}
The painting routine is just intended draw garbage which takes a long time:
For a tightly coupled simulation, javax.swing.Timer is a good choice. Let the timer's listener invoke your implementation of paintComponent(), as shown here and in the example cited here.
For a loosely coupled simulation, let the model evolve in the background thread of a SwingWorker, as shown here. Invoke publish() when apropos to you simulation.
The choice is dictated in part by the nature of the simulation and the duty cycle of the model.
Why not just use stuff from the testbed? It already does everything. Just take the JPanel, controller, and debug draw. It uses Java 2D drawing.
See here for the JPanel that does the buffered rendering:
https://github.com/dmurph/jbox2d/blob/master/jbox2d-testbed/src/main/java/org/jbox2d/testbed/framework/j2d/TestPanelJ2D.java
and here for the debug draw:
https://github.com/dmurph/jbox2d/blob/master/jbox2d-testbed/src/main/java/org/jbox2d/testbed/framework/j2d/DebugDrawJ2D.java
See the TestbedMain.java file to see how the normal testbed is launched, and rip out what you don't need :)
Edits:
Disclaimer: I maintain jbox2d
Here is the package for the testbed framework: https://github.com/dmurph/jbox2d/tree/master/jbox2d-testbed/src/main/java/org/jbox2d/testbed/framework
TestbedMain.java is in the j2d folder, here:
https://github.com/dmurph/jbox2d/tree/master/jbox2d-testbed/src/main/java/org/jbox2d/testbed/framework/j2d