Advertisement
  1. Game Development
  2. Java

Introdução ao JavaFX para Desenvolvimento de Jogos

Scroll to top
Read Time: 17 min

() translation by (you can also view the original English article)

JavaFX é um conjunto de ferramentas de interface de usuário multiplataforma para Java e é o sucessor das bibliotecas Swing. Nesse tutorial, nós exploraremos os recursos do JavaFX que o tornam fácil de usar para iniciar em programação de jogos em Java.

Nesse tutorial assumiremos que você já sabe programar em Java. Caso contrário, confira Aprendendo Java para Android, Introdução à Programação de Computadores Usando Java: 101 e 201, Head First Java, Greenfoot ou Learn Java the Hard Way para começar.

Instalando

Se você já desenvolve aplicativos em Java, provavelmente não precisa baixar nada para começar: O JavaFX é incluído por padrão com no JDK (Kit de Desenvolvimento Java) desde a versão 7u6 de agosto de 2012. Se você não atualiza o Java a algum tempo, visite o site de descarga do Java para obter a última versão.

Classes Básicas do Arcabouço

A criação de um programa JavaFX começa com a implementação da classe Application, que é a superclasse de todos os aplicativos JavaFX. Sua classe principal deve chamar o método launch(), que então chamará init(), então start(), esperará o término do aplicativo e então chamará o método stop(). Dentre estes métodos, apenas start() é abstrato e deve ser sobrescrito.

A classe Stage é o recipiente de nível mais alto no JavaFX. Quando um aplicativo é iniciado, uma instância de Stage é criada e passada para o método start da classe Application. A classe Stage controla propriedades básicas da janela como o título, ícone, visibilidade, redimensionamento, modo de tela inteira e decorações - que são controladas usando a classe StageStyle. Mais instâncias de Stage podem ser criadas se necessário. Depois de configurada a classe Stage, o conteúdo pode ser adicionado à ela e o método show() é chamado.

Sabendo disso, podemos agora criar um exemplo mínimo que mostra uma janela em JavaFX:

1
import javafx.application.Application;
2
import javafx.stage.Stage;
3
4
public class Example1 extends Application 
5
{
6
    public static void main(String[] args) 
7
    {
8
        launch(args);
9
    }
10
11
    public void start(Stage theStage) 
12
    {
13
        theStage.setTitle("Hello, World!");
14
        theStage.show();
15
    }
16
}

Estrutura do Conteúdo

O conteúdo em JavaFX (como texto, imagens e controles) é organizado como uma estrutura de árvore conhecida como grafo de cena, que agrupa e organiza os elementos de uma cena gráfica.

JavaFX Scene GraphJavaFX Scene GraphJavaFX Scene Graph
Representação de um grafo de cena do JavaFX.

Um elemento genérico em um grafo de cena no JavaFX é chamado de Node. Cada instância de Node em uma árvore tem apenas um nó pai, com exceção de um nó especial chamado de raíz. A classe Group é uma especialização de Node que pode ter vários nós filhos. Transformações gráficas (translado, rotação, escala etc) e efeitos aplicados à uma instância de Group também serão aplicadas aos nós filhos. Instâncias de Node podem ser estilizadas usando-se as folhas de estilo em cascata do JavaFX (CSS), muito parecido com o CSS usado com documentos HTML.

A classe Scene contém todo o conteúdo de um grafo de cena e precisa de uma instância de Node para servir de raíz (geralmente a raíz é uma instância de Group). Você pode configurar o tamanho de uma cena, caso contrário, seu tamanho será calculado com base em seu conteúdo. Uma instância de Scene deve ser passada à uma instância de Stage (através do método setScene()) para que possa ser exibida.

Renderizando os Gráficos

A renderização de gráficos é particularmente importante para programadores de jogos! Em JavaFX, a instância de Canvas é uma imagem na qual se pode desenhar texto, formas e imagens, usando a instância de GraphicsContext associada. Para aqueles desenvolvedores familiares com Swing, GraphicsContext é similar à classe Graphics que é parâmetro do método paint() de JComponent.

A instância de GraphicsContext é customizável de várias formas. Para escolher a cor de desenho, você pode configurar as cores de fill (preenchimento) e stroke (borda), que são instâncias de Paint: pode ser Color (cor uniforme), um gradiente (LinearGradient ou RadialGradient) ou mesmo uma ImagePattern (padrão de imagem). Você também pode aplicar um ou mais efeitos, como Lighting, Shadow ou GaussianBlur e mudar as fontes usando a classe Font.

A classe Image facilita o carregamento de imagens de vários formatos e desenha através da classe GraphicsContext. É fácil construir proceduralmente imagens usando a classe WritableImage com as classes PixelReader e PixelWriter;

Usando essas classes, podemos criar um exemplo no melhor estilo "Olá mundo!" como o abaixo. Para ser breve, colocaremos apenas a implementação de start() aqui (pularemos algumas declarações importantes e o método main()). De qualquer forma, o exemplo funcional completo pode ser encontrado no repositório do GitHub que acompanha esse turorial.

1
public void start(Stage theStage) 
2
{
3
    theStage.setTitle( "Canvas Example" );
4
        
5
    Group root = new Group();
6
    Scene theScene = new Scene( root );
7
    theStage.setScene( theScene );
8
        
9
    Canvas canvas = new Canvas( 400, 200 );
10
    root.getChildren().add( canvas );
11
        
12
    GraphicsContext gc = canvas.getGraphicsContext2D();
13
        
14
    gc.setFill( Color.RED );
15
    gc.setStroke( Color.BLACK );
16
    gc.setLineWidth(2);
17
    Font theFont = Font.font( "Times New Roman", FontWeight.BOLD, 48 );
18
    gc.setFont( theFont );
19
    gc.fillText( "Hello, World!", 60, 50 );
20
    gc.strokeText( "Hello, World!", 60, 50 );
21
    
22
    Image earth = new Image( "earth.png" );
23
    gc.drawImage( earth, 180, 100 );
24
        
25
    theStage.show();
26
}

O Loop do Jogo

Em seguida tornaremos nossos programas dinâmicos, ou seja, seu estado se alterará em função do tempo. Implementaremos um loop de jogo: um laço infinito que atualiza os objetos do jogo e renderiza a cena na tela, de preferência a 60 quadros por segundo.

A maneira mais fácil de fazê-lo em JavaFX é usando a classe AnimationTimer, onde o método handle() precisa ser implementado e será chamado a taxa de 60 quadros por segundo, ou o mais próximo dessa taxa possível. O que não quer dizer que só possa ser usado para animações, essa classe é capaz de muito mais.

Usar a classe AnimationTimer pode ser um pouco complicado: como ela é uma classe abstrata, ela não pode ser criada diretamente — a classe deve ser estendida antes que uma instância possa ser criada. De qualquer forma, para nossos exemplos simples. iremos estender nossa classe escrevendo uma classe anônima interna. A classe interna deve definir o método abstrato handle(), que reveberá apenas um argumento: o tempo atual do sistema em nanosegundos. Depois de declarar a classe interna, chamaremos imediatamente o método start(). que iniciará o laço. O laço pode ser interrompido chamando o método stop().

Com essas classes, podemos modificar nosso exemplo "Olá mundo!", criando uma animação consistindo da Terra orbitando o Sol sobre uma imagem de fundo.

1
public void start(Stage theStage) 
2
{
3
    theStage.setTitle( "Timeline Example" );
4
5
    Group root = new Group();
6
    Scene theScene = new Scene( root );
7
    theStage.setScene( theScene );
8
9
    Canvas canvas = new Canvas( 512, 512 );
10
    root.getChildren().add( canvas );
11
12
    GraphicsContext gc = canvas.getGraphicsContext2D();
13
14
    Image earth = new Image( "earth.png" );
15
    Image sun   = new Image( "sun.png" );
16
    Image space = new Image( "space.png" );
17
18
    final long startNanoTime = System.nanoTime();
19
20
    new AnimationTimer()
21
    {
22
        public void handle(long currentNanoTime)
23
        {
24
            double t = (currentNanoTime - startNanoTime) / 1000000000.0; 
25
26
            double x = 232 + 128 * Math.cos(t);
27
            double y = 232 + 128 * Math.sin(t);
28
29
            // background image clears canvas

30
            gc.drawImage( space, 0, 0 );
31
            gc.drawImage( earth, x, y );
32
            gc.drawImage( sun, 196, 196 );
33
        }
34
    }.start();
35
36
    theStage.show();
37
}

Há outras maneiras de implementar o loop do jogo em JavaFX. Uma abordagem um pouco maior (mas mais flexível) é usar a classe Timeline, que é uma sequencia de animação consistindo de uma sequencia de instâncias de KeyFrame. Para criar o loop do jogo, a intância de TimeLine deve ser configurada para repetir indefinidamente, e apenas uma instância de KeyFrame é necessária, com Duration de 0,016 segundos (para obtermos 60 ciclos por segundo). Essa implementação pode ser encontrada no arquivo Example3T.java do repositório do GitHub.

Animação Baseada em Quadros

Outro componente geralmente necessário em programação de jogos é a animação baseada em quadros: mostrar uma sequência de imagens em sucessão rapidamente para criar a iusão de movimento.

Assumindo que todas as animações e todos os quadros têm a mesma duração, uma implementação básica pode ser simples como a abaixo:

1
public class AnimatedImage
2
{
3
    public Image[] frames;
4
    public double duration;
5
    
6
    public Image getFrame(double time)
7
    {
8
        int index = (int)((time % (frames.length * duration)) / duration);
9
        return frames[index];
10
    }
11
}

Para integrar esta classe no exemplo anterior, podemos criar um OVNI animado, iniciando o objeto usando o código:

1
AnimatedImage ufo = new AnimatedImage();
2
Image[] imageArray = new Image[6];
3
for (int i = 0; i < 6; i++)
4
    imageArray[i] = new Image( "ufo_" + i + ".png" );
5
ufo.frames = imageArray;
6
ufo.duration = 0.100;

E, na instância de AnimationTimer, adicionando uma única linha de código:

1
gc.drawImage( ufo.getFrame(t), 450, 25 ); 

No lugar apropriado. Para um exemplo funcional completo, confira o arquivo Example3AI.java no repositório do GitHub.

Manipulando a Entrada do Usuário

Detecar e processar a entrada do usuário em JavaFX é simples. Ações do usuário que podem ser detectadas pelo sistema (como uma tecla pressionada ou um clique) são chamadas de eventos. Em JavaFX, essas ações criam automaticamente objetos (como instâncias de KeyEvent ou MouseEvent) que armazenam os dados associados (como a tecla pressionada ou a posição do ponteiro). Qualquer classe JavaFX que implementa a interface EventTarget (como a classe Scene) pode escutar eventos e manipulá-los. Nos exemplos seguintes, nós mostraremos como configurar uma cena para processar vários eventos.

Na documentação da classe Scene há vários métodos que escutam diferentes tipos de entrada de diferentes fontes. Por exemplo, o método setOnKeyPressed() pode atribuir uma instância de EventHandler que será ativado quando uma tecla for pressionada e o método setOnMouseClicked() pode atribuir uma instância de EventHandler que é ativada quando um botão do mouse for pressionado. A classe EventHandler serve um propósito: encapsular um método (chamado handle()) que é chamado quando o evento correpondente ocorre.

Ao criar uma instância de EventHandler, você deve especificar o tipo de evento que ela manipula, por exemplo, você pode declarar um EventHandler<KeyEvent> ou um EventHandler<MouseEvent>. Instancias de EventHandler são geralmente criados como classes anônimas internas já que normalmente são usadas apenas uma vez (quando são passadas como um argumento de um dos métodos listados acima).

Manipulado Eventos do Teclado

Entrada do usuário são geralmente processadas no loop principal do jogo, e assim, um registro das teclas ativas deve ser mantido. Uma maneira de fazer isso é criar uma instância de ArrayList<String>. Quando uma tecla é pressionada, adicionaremos a representação do KeyCode da classe KeyEvent à lista em forma de String e quando for solta, a removeremos.

No exemplo abaixo, a tela contém duas imagens de setas. Quando uma tecla é pressionada, sua imagem correspondente se torna verde.


O código fonte está no arquivo Exemple4K.java no repositório do GitHub.

1
public void start(Stage theStage) 
2
    {
3
        theStage.setTitle( "Keyboard Example" );
4
5
        Group root = new Group();
6
        Scene theScene = new Scene( root );
7
        theStage.setScene( theScene );
8
9
        Canvas canvas = new Canvas( 512 - 64, 256 );
10
        root.getChildren().add( canvas );
11
12
        ArrayList<String> input = new ArrayList<String>();
13
14
        theScene.setOnKeyPressed(
15
            new EventHandler<KeyEvent>()
16
            {
17
                public void handle(KeyEvent e)
18
                {
19
                    String code = e.getCode().toString();
20
21
                    // only add once... prevent duplicates

22
                    if ( !input.contains(code) )
23
                        input.add( code );
24
                }
25
            });
26
27
        theScene.setOnKeyReleased(
28
            new EventHandler<KeyEvent>()
29
            {
30
                public void handle(KeyEvent e)
31
                {
32
                    String code = e.getCode().toString();
33
                    input.remove( code );
34
                }
35
            });
36
37
        GraphicsContext gc = canvas.getGraphicsContext2D();
38
39
        Image left = new Image( "left.png" );
40
        Image leftG = new Image( "leftG.png" );
41
42
        Image right = new Image( "right.png" );
43
        Image rightG = new Image( "rightG.png" );
44
45
        new AnimationTimer()
46
        {
47
            public void handle(long currentNanoTime)
48
            {
49
                // Clear the canvas

50
                gc.clearRect(0, 0, 512,512);
51
52
                if (input.contains("LEFT"))
53
                    gc.drawImage( leftG, 64, 64 );
54
                else
55
                    gc.drawImage( left, 64, 64 );
56
57
                if (input.contains("RIGHT"))
58
                    gc.drawImage( rightG, 256, 64 );
59
                else
60
                    gc.drawImage( right, 256, 64 );
61
            }
62
        }.start();
63
64
        theStage.show();
65
    }

Manipulando Eventos do Mouse

Agora vamos dar uma olhada em um exemplo que foca na classe MouseEvent em vez de KeyEvent. Nesse minijogo o jogador ganha pontos cada vez que o alvo é clicado.


Como EventHandler é implementado como uma classe anônima interna, qualquer variável usada por ela deve ser final ou "efetivamente final", significando que a variável não pode ser reatribuída. No exemplo anterior, os dados são passados ao EventHandler através do ArrayList, no qual os valores podem ser mudados sem reinicializá-lo (através dos métodos add() e remove()).

Porém para tipos básicos de dados, os valores não podem ser mudados depois de inicializados. Se você gostaria que o EventHandler podesse acessar tipos básicos de dados que mudam, você pode criar uma classe empacotadora que tenha variáveis públicas ou métodos para modificar o valor. No exemplo abaixo a classe IntValue possui uma variável pública do tipo int chamada value.

1
public void start(Stage theStage) 
2
{
3
    theStage.setTitle( "Click the Target!" );
4
5
    Group root = new Group();
6
    Scene theScene = new Scene( root );
7
    theStage.setScene( theScene );
8
9
    Canvas canvas = new Canvas( 500, 500 );
10
11
    root.getChildren().add( canvas );
12
13
    Circle targetData = new Circle(100,100,32);
14
    IntValue points = new IntValue(0);
15
16
    theScene.setOnMouseClicked(
17
        new EventHandler<MouseEvent>()
18
        {
19
            public void handle(MouseEvent e)
20
            {
21
                if ( targetData.containsPoint( e.getX(), e.getY() ) )
22
                {
23
                    double x = 50 + 400 * Math.random(); 
24
                    double y = 50 + 400 * Math.random();
25
                    targetData.setCenter(x,y);
26
                    points.value++;
27
                }
28
                else
29
                    points.value = 0;
30
            }
31
        });
32
33
    GraphicsContext gc = canvas.getGraphicsContext2D();
34
35
    Font theFont = Font.font( "Helvetica", FontWeight.BOLD, 24 );
36
    gc.setFont( theFont );
37
    gc.setStroke( Color.BLACK );
38
    gc.setLineWidth(1);
39
40
    Image bullseye = new Image( "bullseye.png" );
41
42
    new AnimationTimer()
43
    {
44
        public void handle(long currentNanoTime)
45
        {
46
            // Clear the canvas

47
            gc.setFill( new Color(0.85, 0.85, 1.0, 1.0) );
48
            gc.fillRect(0,0, 512,512);
49
50
            gc.drawImage( bullseye, 
51
                targetData.getX() - targetData.getRadius(),
52
                targetData.getY() - targetData.getRadius() );
53
54
            gc.setFill( Color.BLUE );
55
56
            String pointsText = "Points: " + points.value;
57
            gc.fillText( pointsText, 360, 36 );
58
            gc.strokeText( pointsText, 360, 36 );
59
        }
60
    }.start();
61
62
    theStage.show();
63
}

O código fonte completo está no repositório do GitHub, a classe principal é Example4M.java

Criando uma Classe Básica de Sprite com JavaFX

Em jogos eletrônicos, sprite é o termo que designa uma entidade visual. Abaixo temos um exemplo de uma classe de sprite que armazena uma imagem e uma posição, assim como a velocidade (para entidades móveis) e o tamanho para calcular as caixas delimitantes usadas para detecção de colisão. Nós também temos os métodos para obter e atribuir esses dados (omitidos para manter a brevidade), e alguns métodos necessários para o desenvolvimento de jogos:

  • update(): calcula a nova posição baseado na velocidade do sprite.
  • render(): desenha a imagem associada à tela (através da classe GraphicsContext) na posição armazenada no sprite.
  • getBoundary(): retorna uma instância de Rectangle2D do JavaFX, útil na detecção de colisão através do método intersects.
  • intersects(): determina se a caixa delimitante desse sprite intersecta com outro sprite.
1
public class Sprite
2
{
3
    private Image image;
4
    private double positionX;
5
    private double positionY;    
6
    private double velocityX;
7
    private double velocityY;
8
    private double width;
9
    private double height;
10
11
    // ...

12
    // methods omitted for brevity

13
    // ...

14
15
    public void update(double time)
16
    {
17
        positionX += velocityX * time;
18
        positionY += velocityY * time;
19
    }
20
21
    public void render(GraphicsContext gc)
22
    {
23
        gc.drawImage( image, positionX, positionY );
24
    }
25
26
    public Rectangle2D getBoundary()
27
    {
28
        return new Rectangle2D(positionX,positionY,width,height);
29
    }
30
31
    public boolean intersects(Sprite s)
32
    {
33
        return s.getBoundary().intersects( this.getBoundary() );
34
    }
35
}

O código fonte completo está em Sprite.java no repositório do GitHub.

Usando a Classe de Sprite

Com a ajuda da classe Sprite, podemos facilmente criar um jogo de coleta em JavaFX. Nesse jogo, você assume o papel de uma maleta consciente que tem como objetivo coletar sacolas de dinheiro que foram deixadas no chão pelo antigo dono descuidado. As setas controlam a maleta pela tela.

Esse código recebe muita influência dos exemplos anteriores: configurando uma fonte para mostrar a pontuação, armazenando a entrada do teclado em uma ArrayList, implementando o loop do jogo com um AnimationTImer e criando classes empacotadoras para valores simples que precisam ser modificados no loop do jogo.

Um trecho do código interessante envolve criar um objeto para o personagem (maleta) e a ArrayList de instâncias de Sprite para os colecionáveis (sacolas de dinheiro):

1
Sprite briefcase = new Sprite();
2
briefcase.setImage("briefcase.png");
3
briefcase.setPosition(200, 0);
4
5
ArrayList<Sprite> moneybagList = new ArrayList<Sprite>();
6
7
for (int i = 0; i < 15; i++)
8
{
9
    Sprite moneybag = new Sprite();
10
  moneybag.setImage("moneybag.png");
11
	double px = 350 * Math.random() + 50;
12
	double py = 350 * Math.random() + 50;          
13
	moneybag.setPosition(px,py);
14
	moneybagList.add( moneybag );
15
}

Outro trecho interessante é o de criação do AnimationTimer, que tem como função:

  • calcular o tempo passado desde a última atualização
  • configurar a velocidade do jogador dependendo das teclas pressionadas
  • executar a detecção de colisão entre o jogador e os colecionáveis e atualizar a pontuação e a lista de colecionáveis quando ocorrer a colisão (um iterador é usado em vez de acessar o ArrayList diretamente para evitar uma exceção de modificação concorrente ao remover os objetos da lista)
  • renderizar os sprites e o texto na tela
1
new AnimationTimer()
2
{
3
    public void handle(long currentNanoTime)
4
	{
5
		// calculate time since last update.

6
		double elapsedTime = (currentNanoTime - lastNanoTime.value) / 1000000000.0;
7
		lastNanoTime.value = currentNanoTime;
8
		
9
		// game logic

10
		
11
		briefcase.setVelocity(0,0);
12
		if (input.contains("LEFT"))
13
			briefcase.addVelocity(-50,0);
14
		if (input.contains("RIGHT"))
15
			briefcase.addVelocity(50,0);
16
		if (input.contains("UP"))
17
			briefcase.addVelocity(0,-50);
18
		if (input.contains("DOWN"))
19
			briefcase.addVelocity(0,50);
20
			
21
		briefcase.update(elapsedTime);
22
		
23
		// collision detection

24
		
25
		Iterator<Sprite> moneybagIter = moneybagList.iterator();
26
		while ( moneybagIter.hasNext() )
27
		{
28
			Sprite moneybag = moneybagIter.next();
29
			if ( briefcase.intersects(moneybag) )
30
			{
31
				moneybagIter.remove();
32
				score.value++;
33
			}
34
		}
35
		
36
		// render

37
		
38
		gc.clearRect(0, 0, 512,512);
39
		briefcase.render( gc );
40
		
41
		for (Sprite moneybag : moneybagList )
42
			moneybag.render( gc );
43
44
		String pointsText = "Cash: $" + (100 * score.value);
45
		gc.fillText( pointsText, 360, 36 );
46
		gc.strokeText( pointsText, 360, 36 );
47
	}
48
}.start();

Como de costume, o código pode ser encontrado em um arquivo (Example5.java) do repositório do GitHub.

Próximos Passos

Conclusão

Nesse tutorial, eu te apresentei à classes JavaFX que são úteis em programação de jogos. Nós passamos por uma série de exemplos de complexidade ascendente, culminando em um jogo de coleta com sprites. Agora você está pronto para investigar alguns recursos listados acima ou mergulhar e começar a criação do seu próprio jogo. Muito boa sorte para você em seu empenho!

Seja o primeiro a saber sobre novas traduções–siga @tutsplus_pt no Twitter!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Game Development tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.