Well…
Estou postando aqui um código “pequeno”, mas completo, com um mini framework capaz de possibilitar animações e jogos em Java 2D acelerado, que pode ser modificado para quaisquer outras animações ou jogos, já habilitado para receber entrada do usuário via teclado.
Isso é resultado de um bom tempo de pesquisa e estudo que fiz. Vou colocar sem comentários pra deixar quem se interessar quebrar a cabeça um pouco, que é o melhor jeito de aprender, e respondo as dúvidas que aparecerem!
Em anexo o jar funcional desse programa (auto-executável, basta ter uma JRE 6 instalada e clicar duas vezes) com os fontes inclusos.
Tudo começa com a janela principal:
[code]package view;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsEnvironment;
import java.awt.Insets;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.RenderingHints.Key;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.util.HashMap;
import java.util.Map;
import javax.swing.JFrame;
import controller.Engine;
import controller.EngineImpl;
public class MainWindow extends JFrame implements Runnable {
private static final long serialVersionUID = 1L;
public static final String TITLE = "Soft";
public static final int NUM_BUFFERS = 2;
public static final int WIDTH = 160;
public static final int HEIGHT = 120;
public static final int FPS = 60;
public static final int MIN_PAUSE = 5;
private static Map<Key, Object> renderingHints;
private BufferedImage buffer;
private Graphics2D g2;
private volatile boolean running;
private Engine engine;
public MainWindow() {
super(TITLE);
engine = new EngineImpl();
engine.init();
configBuffer();
configWindow();
}
private void configBuffer() {
buffer = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration().createCompatibleImage(WIDTH, HEIGHT);
g2 = buffer.createGraphics();
g2.setRenderingHints(getRenderingHints());
}
private void configWindow() {
this.pack();
Insets it = this.getInsets();
this.setSize(WIDTH+it.left+it.right, HEIGHT+it.top+it.bottom);
this.setResizable(false);
this.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
this.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent e) {
MainWindow.this.stop();
}
});
this.setFocusable(true);
this.addFocusListener(new FocusAdapter(){
public void focusLost(FocusEvent e) {
MainWindow.this.requestFocus();
}
});
this.addKeyListener(engine);
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
this.setLocation((int)((screenSize.getWidth()-this.getWidth())/2), (int)((screenSize.getHeight()-this.getHeight())/2));
}
public void run() {
this.setVisible(true);
this.createBufferStrategy(NUM_BUFFERS);
running = true;
while(running) {
long initialTime = System.nanoTime();
update();
paintBuffer();
prepareBufferStrategy();
try {
Thread.sleep(calculateSleepTime(initialTime));
} catch (InterruptedException e) {}
this.getBufferStrategy().show();
}
System.exit(0);
}
private void update() {
engine.update();
}
private void paintBuffer() {
clearScreen();
engine.paint(g2);
}
private void clearScreen() {
g2.setColor(Color.BLACK);
g2.fillRect(0, 0, WIDTH, HEIGHT);
}
private void prepareBufferStrategy() {
Graphics g = this.getBufferStrategy().getDrawGraphics();
g.translate(this.getInsets().left, this.getInsets().top);
g.setClip(0, 0, WIDTH, HEIGHT);
g.drawImage(buffer, 0, 0, null);
}
private long calculateSleepTime(long initialTime) {
long pauseTime = 1000/FPS-(initialTime-System.nanoTime())/1000000;
return pauseTime>MIN_PAUSE?pauseTime:MIN_PAUSE;
}
private void stop() {
running = false;
}
public static void main(String[] args) {
new Thread(new MainWindow()).start();
}
public static Map<Key, Object> getRenderingHints() {
if(renderingHints == null) {
renderingHints = new HashMap<Key, Object>();
renderingHints.put(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
renderingHints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
renderingHints.put(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
renderingHints.put(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE);
renderingHints.put(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
renderingHints.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
renderingHints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
renderingHints.put(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE);
renderingHints.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
}
return renderingHints;
}
}[/code]
Essa janela delega o controle da animação / interatividade a um engine que segue esse contrato:
[code]package controller;
import java.awt.Graphics2D;
import java.awt.event.KeyListener;
public interface Engine extends KeyListener {
public void init();
public void update();
public void paint(Graphics2D g2);
}[/code]
Tornando a implementação do engine livre dos detalhes de nível mais baixo. A implementação sugerida aqui é essa:
[code]package controller;
import java.awt.Graphics2D;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.List;
import model.Ball;
import model.Sprite;
public class EngineImpl implements Engine {
private List<Sprite> sprites;
public void init() {
sprites = new ArrayList<Sprite>();
sprites.add(new Ball());
}
public void update() {
for(Sprite sprite : sprites) {
sprite.update();
}
}
public void paint(Graphics2D g2) {
for(Sprite sprite : sprites) {
sprite.paint(g2);
}
}
public void keyPressed(KeyEvent e) {}
public void keyReleased(KeyEvent e) {}
public void keyTyped(KeyEvent e) {}
}[/code]
Trabalhando sempre com uma lista de Sprites, facilitando muito a abstração. Cada objeto que herde de Sprite poderá ser colocado como caixa preta nesse engine.
Futuramente o desenvolvedor pode fazer os eventos recebidos refletirem no engine e nos sprites com os métodos implementados do KeyListener.
Vejam o que define a classe Sprite (um Sprite é um objeto de tela na animação ou jogo, e pode ser interativo):
[code]package model;
import java.awt.Graphics2D;
public abstract class Sprite {
protected int width;
protected int height;
protected double posX;
protected double posY;
protected double velX;
protected double velY;
protected boolean active;
protected boolean visible;
public Sprite() {
restore();
}
public abstract void update();
public abstract void paint(Graphics2D g2);
public void suspend() {
setActive(false);
setVisible(false);
}
public void restore() {
setActive(true);
setVisible(true);
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public double getPosX() {
return posX;
}
public void setPosX(double posX) {
this.posX = posX;
}
public double getPosY() {
return posY;
}
public void setPosY(double posY) {
this.posY = posY;
}
public double getVelX() {
return velX;
}
public void setVelX(double velX) {
this.velX = velX;
}
public double getVelY() {
return velY;
}
public void setVelY(double velY) {
this.velY = velY;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
public boolean isVisible() {
return visible;
}
public void setVisible(boolean visible) {
this.visible = visible;
}
}[/code]
E a implementação proposta com a finalidade de oferecer uma visualização funcional:
[code]package model;
import java.awt.Color;
import java.awt.Graphics2D;
import java.util.Random;
import view.MainWindow;
public class Ball extends Sprite {
private Color color;
private boolean goingRight;
private boolean goingDown;
public Ball() {
Random random = new Random();
color = new Color(0x0088FF);
width = height = (int)(0.1 * MainWindow.HEIGHT);
posX = random.nextInt(MainWindow.WIDTH - width);
posY = random.nextInt(MainWindow.HEIGHT - height);
velX = MainWindow.HEIGHT/2;
velY = MainWindow.WIDTH/2;
goingRight = random.nextInt(2)==0;
goingDown = random.nextInt(2)==0;
}
public void update() {
if(!active)return;
if(goingRight) {
posX += velX/MainWindow.FPS;
if(posX > MainWindow.WIDTH - width) {
posX = MainWindow.WIDTH - width;
goingRight = false;
}
} else {
posX -= velX/MainWindow.FPS;
if(posX < 0) {
posX = 0;
goingRight = true;
}
}
if(goingDown) {
posY += velY/MainWindow.FPS;
if(posY > MainWindow.HEIGHT - height) {
posY = MainWindow.HEIGHT - height;
goingDown = false;
}
} else {
posY -= velY/MainWindow.FPS;
if(posY < 0) {
posY = 0;
goingDown = true;
}
}
}
public void paint(Graphics2D g2) {
if(!visible)return;
g2.setColor(color);
g2.fillOval((int)posX, (int)posY, width, height);
}
}[/code]
Vamos ver se alguém se interessa em entender esse código!