I\'m programming a platform game in Java, and I am hand-coding the player animations. I am animating each limb separately, changing the position and rotation. This works fin
So the basic idea is to simply flip/mirror the Graphics
context by scaling one of the axis by a negative value (-1
).
This can be accomplished by simply using Graphics#scale
, for example, to flip the graphics horizontally, you would simply use
graphics.scale(-1, 1);
You would need to translate the Graphics
by the width of the viewable area as well using Graphics#translate
to reposition the image within the viewable area.
Everything painted after this will be affected by the change. Because of this, you should be taking snapshots of the Graphics
context before every significant change, just make sure you dispose
of it when you're done...
Here is a (rather pathetic animation wise) example...Basically, when you press the left or right arrows, a flag is flipped and the scene repainted. Depending on the flag, the Graphics
context is flipped/mirrored...
import java.awt.BorderLayout;
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.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import javax.imageio.ImageIO;
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 AnimateCharacter {
public static void main(String[] args) {
new AnimateCharacter();
}
public AnimateCharacter() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
private BufferedImage body;
private BufferedImage[] legs;
private double[] angels;
private double[] deltas;
private Point[] pivots = new Point[]{
// Foreground...
new Point(100, 227), // Foreleg
new Point(155, 201), // Hindleg
// Background...
new Point(93, 218), // Foreleg
new Point(143, 195), // Hindleg
};
private Point[] locations = new Point[]{
// Foreground...
new Point(67, 221), // Foreleg
new Point(124, 172), // Hindleg
// Background...
new Point(60, 219), // Foreleg
new Point(112, 166), // Hindleg
};
private int direction = 1;
public TestPane() {
legs = new BufferedImage[4];
angels = new double[]{
45,
40,
-5,
0
};
deltas = new double[]{
-4,
-4,
4,
4
};
try {
body = ImageIO.read(getClass().getResource("/Body.png"));
// Foreground...
legs[0] = ImageIO.read(getClass().getResource("/ForeLeg.png"));
legs[1] = ImageIO.read(getClass().getResource("/HindLeg.png"));
// Background...
legs[2] = ImageIO.read(getClass().getResource("/ForeLeg.png"));
legs[3] = ImageIO.read(getClass().getResource("/HindLeg.png"));
} catch (IOException exp) {
exp.printStackTrace();
}
Timer timer = new Timer(40, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
for (int index = 0; index < angels.length; index++) {
angels[index] += deltas[index];
if (angels[index] < -45) {
angels[index] = -45;
deltas[index] *= -1;
} else if (angels[index] > 45) {
angels[index] = 45;
deltas[index] *= -1;
}
}
repaint();
}
});
timer.start();
InputMap im = getInputMap(WHEN_IN_FOCUSED_WINDOW);
ActionMap am = getActionMap();
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), "left");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), "right");
am.put("left", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
direction = 1;
repaint();
}
});
am.put("right", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
direction = -1;
repaint();
}
});
}
@Override
public Dimension getPreferredSize() {
return body == null ? new Dimension(200, 200) : new Dimension(body.getWidth() + 50, body.getHeight() + 50);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
applyQualityRenderingHints(g2d);
int x = (getWidth() - body.getWidth()) / 2;
int y = (getHeight() - body.getHeight()) / 2;
if (direction < 0) {
g2d.scale(-1, 1);
g2d.translate(-getWidth(), 0);
}
// Background legs...
drawLegs(g2d, x, y, 2);
g2d.drawImage(body, x, y, this);
// Foreground legs...
drawLegs(g2d, x, y, 0);
g2d.dispose();
}
protected void drawLegs(Graphics2D g2d, int x, int y, int offset) {
for (int index = 0; index < 2; index++) {
Graphics2D copy = (Graphics2D) g2d.create();
copy.translate(x, y);
int leg = index + offset;
int pivotX = pivots[leg].x;
int pivotY = pivots[leg].y;
copy.rotate(Math.toRadians(angels[leg]), pivotX, pivotY);
copy.drawImage(
legs[leg],
locations[leg].x,
locations[leg].y,
this);
copy.dispose();
}
}
public void applyQualityRenderingHints(Graphics2D g2d) {
g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
// g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
}
}
}
The magic happens in a few places...
The Graphics
context is first copied and then (if required) flipped using scale
...
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
//...
if (direction < 0) {
g2d.scale(-1, 1);
g2d.translate(-getWidth(), 0);
}
I also use the same technique when painting the legs...
protected void drawLegs(Graphics2D g2d, int x, int y, int offset) {
for (int index = 0; index < 2; index++) {
Graphics2D copy = (Graphics2D) g2d.create();
copy.translate(x, y);
//...
copy.rotate(Math.toRadians(angels[leg]), pivotX, pivotY);
copy.drawImage(
legs[leg],
locations[leg].x,
locations[leg].y,
this);
copy.dispose();
}
This isolates the changes to the copy of the Graphics
context, each copy will inherit the current state of it's parent, making this a really useful technique...