Animações e Jogos

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.

:wink:

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!

opa, bacana sua iniciativa em compartilhar o que fez …

peguei o .jar ae, depois dou uma olhada nos fontes :slight_smile:

vlw
[]´s

Codificação simples, programa simples.
Parabens!

Só acho que seria muito mais simples de aprender documentando seu código, mas acredito que você queira “ibope” por isso =)

Parabéns, muito bom! Mesmo sem comentários está fácil de entender. Um código limpo, bem escrito e, principalmente, funcional. :slight_smile:

Sem dúvida pode ser adaptado para animações facilmente, e quanto a performance, está bem satisfatória.

Vou olhar o código e se tiver alguma sugestão postarei aqui!

[quote=Matheus Leandro Ferreira]Codificação simples, programa simples.
Parabens!

Só acho que seria muito mais simples de aprender documentando seu código, mas acredito que você queira “ibope” por isso =)[/quote]
Obrigado, rs!

:slight_smile:

Estou reunindo forças para fazer um tutorial de desenvolvimento de jogos, e a idéia é colocar as idéias principais sem muita firula, exemplificando com um jogo completo, o que vai permitir que um desenvolvedor dedicado consiga usar isso como “start point” para desenvolver qualquer jogo 2D.

Tem muito conceito embutido nesse código, daria pra escrever um livro só com os conceitos que tem aí, de Swing a Design Patterns passando por Threads, Java2D e teoria de animações e jogos.

Mas alguém que se empolgue com a idéia poderá aprender muito entendendo esse código e tirando as dúvidas, e a cada dúvida que surgir darei uma explicação rica e completa do que está acontecendo!

:wink:

[quote=marcobiscaro2112]Parabéns, muito bom! Mesmo sem comentários está fácil de entender. Um código limpo, bem escrito e, principalmente, funcional. :slight_smile:

Sem dúvida pode ser adaptado para animações facilmente, e quanto a performance, está bem satisfatória.

Vou olhar o código e se tiver alguma sugestão postarei aqui![/quote]
Thank you very much!

Tem uns belos truques aí de performance, incluindo o uso de BufferStrategy e de um “pré-buffer” acelerado na memória da placa de vídeo com a invocação do “createCompatibleImage”.

Inclusive se for modificado para ficar em tela cheia com resoluções bem mais altas (1024 x 768, por exemplo), vai rodar facilmente em 60 fps (meus testes assim já chegaram em 250 fps) mesmo que seja um jogo mais complexo, como um Mario World.

E suporta canal alpha real (png e transparências) sem perda de velocidade!

:stuck_out_tongue:

Se eu quizer controlar a bolinha pelo teclado, que classe altero?

Bem resolvi usar um keyevent dentro do main, que chama direntes eventos na ball dependendo da seta precionada, ai assim ela vai pra onde eu quero.

Que vc acha disto? POG?

Tambem coloquei a bolinha pra ir mudando de cor enquanto anda pela tela! Ficou bem psicodelico.

Não faça isso. Isso pode criar conflitos entre a Thread do AWT e essa outra Thread que você criou. Faça assim:

Não faça isso. Isso pode criar conflitos entre a Thread do AWT e essa outra Thread que você criou. Faça assim:

Thank you very much!

Uma coisa que faltou na inicialização da janela que também faz toda a diferença para a performance é isso:

this.setIgnoreRepaint(true);

Bem resolvi usar um keyevent dentro do main, que chama direntes eventos na ball dependendo da seta precionada, ai assim ela vai pra onde eu quero.

Que vc acha disto? POG?

Tambem coloquei a bolinha pra ir mudando de cor enquanto anda pela tela! Ficou bem psicodelico.[/quote]
POG total!

:lol:

Já existem os três métodos implementados do KeyListener na classe EngineImpl:

public void keyPressed(KeyEvent e) {} public void keyReleased(KeyEvent e) {} public void keyTyped(KeyEvent e) {}
Eles já recebem os eventos de teclado do usuário.

Você pode manter a bola na lista, mas também referenciá-la em outra variável, e poderá chamar métodos dela de dentro dos métodos implementados do KeyListener:

[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;
private Ball ball;

public void init() {
	sprites = new ArrayList<Sprite>();
	ball = new Ball();
	sprites.add(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) {
	switch(e.getKeyCode()){
		case KeyEvent.VK_UP:
			// ball.algumaAcao();
			break;
		case KeyEvent.VK_DOWN:
			// ball.algumaAcao();
			break;
		case KeyEvent.VK_LEFT:
			// ball.algumaAcao();
			break;
		case KeyEvent.VK_RIGHT:
			// ball.algumaAcao();
			break;
	}
}
public void keyReleased(KeyEvent e) {}
public void keyTyped(KeyEvent e) {}

}[/code]

[quote=Fox McCloud]
Estou reunindo forças para fazer um tutorial de desenvolvimento de jogos, e a idéia é colocar as idéias principais sem muita firula, exemplificando com um jogo completo, o que vai permitir que um desenvolvedor dedicado consiga usar isso como “start point” para desenvolver qualquer jogo 2D.[/quote]

opa…muito bacana…

só peço que avise aqui do seu tutorial… não quero perde-lo por nada…

tenho vontade de aprender isso quando crescer… ^^

Bem resolvi usar um keyevent dentro do main, que chama direntes eventos na ball dependendo da seta precionada, ai assim ela vai pra onde eu quero.

Que vc acha disto? POG?

Tambem coloquei a bolinha pra ir mudando de cor enquanto anda pela tela! Ficou bem psicodelico.[/quote]
POG total!

:lol:

Já existem os três métodos implementados do KeyListener na classe EngineImpl:

public void keyPressed(KeyEvent e) {} public void keyReleased(KeyEvent e) {} public void keyTyped(KeyEvent e) {}
Eles já recebem os eventos de teclado do usuário.

Você pode manter a bola na lista, mas também referenciá-la em outra variável, e poderá chamar métodos dela de dentro dos métodos implementados do KeyListener:

[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;
private Ball ball;

public void init() {
	sprites = new ArrayList<Sprite>();
	ball = new Ball();
	sprites.add(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) {
	switch(e.getKeyCode()){
		case KeyEvent.VK_UP:
			// ball.algumaAcao();
			break;
		case KeyEvent.VK_DOWN:
			// ball.algumaAcao();
			break;
		case KeyEvent.VK_LEFT:
			// ball.algumaAcao();
			break;
		case KeyEvent.VK_RIGHT:
			// ball.algumaAcao();
			break;
	}
}
public void keyReleased(KeyEvent e) {}
public void keyTyped(KeyEvent e) {}

}[/code][/quote]

Pois é depois que postei q vi os metodos ai no engine, ai mudei pra lah.