8 de fev. de 2016

Um jogo em JavaFX

Há uns meses atrás eu falei sobre jogos para JavaFX e prometi me aprofundar no tema, mas infelizmente o tempo está muito curto e resolvi encurtar a série postando a tradução de um artigo que escrevi para meu blog em inglês, o FXApps: Stacker Game in JavaFX.

O jogo Stacker


O objetivo do jogo Stacker (empilhador)é chegar até o topo empilhando tijolinhos. Em shoppings, há uma versão desse jogo na qual quando se chega no topo você ganha brindes. Vejam esse vídeo do jogo que eu achei no youtube:



O jogo que vou mostrar aqui é um pouco diferente. Iremos amontoar somente um retângulo, mas o retângulo pode estar próximo ao retângulo de baixo, não exatamente no topo dele. O número de retângulos para preencher e a velocidade vão aumentar de acordo com o nível do jogo.


O jogo em Processing 

O jogo foi anteriormente criado em Processing, mas a maioria do código eu pude reusar na versão em JavaFX!
Você pode encontrar a versão do jogo em processing no meu github.
Stacker


Criando jogos em JavaFX


Como já falei em uma postagem anterior, JavaFX é uma  boa plataforma para jogos desde sua versão 1.0. Existia até um blog específico para jogos com JavaFX Script e temos também alguns exemplos que podem ser baixados no site da Oracle. 
Como já sabemos, JavaFX tem todos os recursos para criamos jogos: animações, detecção de colisão, efeitos especiais, tocar sons, etc.

Nossa implementação do Stacker Game


O jogo é basicamente baseado na atualização de uma matriz booleana e na leitura da mesma para criar uma representação visual. Quando o usuário clica no jogo, nós fazer um loop na matriz para verificar se a linha anterior é true ou se o bloco atual (o que se mexe) está adjacente a um bloco na linha anterior.
As duas classes mais importantes no nosso programa é a GameEngine  e Game em sí. A classe game engine é responsável em fazer o jogo à vida, pois ela irá fazer o jogo atualizar a tela numa determinada frequência e fazer ele se desenhar.
O jogo é responsabilizado a desenhar e atualizar o cenário de acordo com o tempo e a entrada do usuário. Aqui está o código dessas duas classes:

package org.jugvale.javafx;
import javafx.scene.canvas.GraphicsContext;
/**
*
* @author william
*/
public abstract class Game {
/*
The GraphicsContext so the game can draw stuff
*/
GraphicsContext gc;
GameEngine engine;
/*
The size of the game playing area
*/
float MAX_W;
float MAX_H;
public Game(float w, float h, GraphicsContext _gc) {
MAX_W = w;
MAX_H = h;
gc = _gc;
gc.getCanvas().setWidth(w);
gc.getCanvas().setHeight(h);
}
final public void setEngine(GameEngine _engine) {
engine = _engine;
}
public abstract void update();
public abstract void display();
}
view raw Game.java hosted with ❤ by GitHub
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package org.jugvale.javafx;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.event.Event;
import javafx.util.Duration;
/**
*
* @author william
*/
public class GameEngine {
private int frameCount = 0;
private int frameRate;
private final Game game;
private final Timeline gameLoop;
public GameEngine(int frameRate, Game game) {
this.frameRate = frameRate;
this.game = game;
game.setEngine(this);
gameLoop = createLoop();
}
public int getFrameCount() {
return frameCount;
}
public int getFrameRate() {
return frameRate;
}
public void setFrameRate(int frameRate) {
this.frameRate = frameRate;
}
private void run(Event e) {
frameCount++;
game.update();
game.display();
}
public void start() {
gameLoop.playFromStart();
}
public void stop() {
gameLoop.stop();
}
private Timeline createLoop() {
// inspired on https://carlfx.wordpress.com/2012/04/09/javafx-2-gametutorial-part-2/
final Duration d = Duration.millis(1000 / frameRate);
final KeyFrame oneFrame = new KeyFrame(d, this::run);
Timeline t = new Timeline(frameRate, oneFrame);
t.setCycleCount(Animation.INDEFINITE);
return t;
}
}
view raw GameEngine.java hosted with ❤ by GitHub
Essas classes não fazem nada! Precisamos criar as classes concretas, então criamos a classe StackerGame que contém a matriz que representa o jogo e desenhará a grade e os retângulos de acordo com o tempo, nível e a entrada do usuário.
A lógica do jogo é basicamente mover um booleano na matriz e ajustar isso qunado o usuário clica. Se o clique do usuário foi feito quando o retângulo tem um bloco abaixo ou adjancente, o jogo vai continuar. Se não, iremos chamar o fim do jogo e quando o usuário clicar again, o jogo vai recomeçar. Note que a pontua;áo do jogo é maior quando o usuário faz uma pilha perfeita e menor se o usuário faz a pilha usando um bloco adjacente.O jogo é desenhado em um canvas, que facilita o controle no nosso caso(que estamos vindo do Processing!).
Notem que nós expomos algumas variáveis usando propriedade JaavaFX. Essas propriedades são usadas em nossa aplicação principal para mostrar a pontuação, nível e eventualmente o label de fim de jogo. Veja o código dessas duas classes:

package org.jugvale.javafx;
import javafx.animation.FadeTransition;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.control.Label;
import javafx.scene.effect.DropShadow;
import javafx.scene.effect.InnerShadow;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontPosture;
import javafx.scene.text.FontWeight;
import javafx.scene.text.TextAlignment;
import javafx.stage.Stage;
import javafx.util.Duration;
/**
*
* @author william
*/
public class App extends Application {
final int WIDTH = 600;
final int HEIGHT = 500;
@Override
public void start(Stage stage) {
Canvas c = new Canvas();
VBox root = new VBox(10);
Scene s = new Scene(new StackPane(root), WIDTH, HEIGHT);
StackerGame game = new StackerGame(400, 300, c.getGraphicsContext2D());
GameEngine stackerGameEngine = new GameEngine(1000, game);
Label lblTitle = new Label("The Stacker Game");
Label lblGameOver = new Label("Game Over! \nClick to play again...");
Label lblScore = new Label();
Label lblLevel = new Label();
lblGameOver.visibleProperty().bind(game.gameOver);
lblScore.textProperty().bind(new SimpleStringProperty("Score is ").concat(game.score));
lblLevel.textProperty().bind(new SimpleStringProperty("Level ").concat(game.level));
// could be done using CSS
lblGameOver.setTextAlignment(TextAlignment.CENTER);
lblScore.setFont(Font.font(STYLESHEET_MODENA, FontWeight.EXTRA_LIGHT, FontPosture.ITALIC, 25));
lblScore.setTextFill(Color.BLUE);
lblLevel.setTextFill(Color.GREEN);
lblGameOver.setFont(Font.font("Arial", FontWeight.EXTRA_BOLD, FontPosture.ITALIC, 35));
lblGameOver.setEffect(new InnerShadow(10, Color.DARKRED));
lblTitle.setEffect(new DropShadow(20, Color.RED));
FadeTransition gameOverAnimation = new FadeTransition(Duration.millis(500), lblGameOver);
gameOverAnimation.setFromValue(0.1);
gameOverAnimation.setToValue(1);
gameOverAnimation.setCycleCount(-1);
gameOverAnimation.setAutoReverse(true);
gameOverAnimation.play();
lblGameOver.setOnMouseClicked(e -> game.restart());
game.gameOver.addListener((vl, o, n) -> {
if (n) {
c.setOpacity(0.3);
} else {
c.setOpacity(1);
}
});
root.getChildren().addAll(lblTitle, new StackPane(c, lblGameOver), lblLevel, lblScore);
root.setAlignment(Pos.CENTER);
lblTitle.setFont(Font.font(30));
s.setFill(Color.LIGHTGRAY);
stage.setScene(s);
stage.show();
stackerGameEngine.start();
}
}
view raw App.java hosted with ❤ by GitHub
package org.jugvale.javafx;
import java.util.Random;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;
class StackerGame extends Game {
/*
The number of points that can be awarded
*/
int MAX_POINTS = 15;
int MIN_POINTS = 5;
/*
Indicates when the player lose the game
*/
public BooleanProperty gameOver;
/*
The total of points
*/
public IntegerProperty score;
int INITIAL_COL = 3;
int INITIAL_LINES = 2;
/*
Initial number of lines and columns which will change according to the game level
*/
int _LINES = INITIAL_LINES;
int _COLUMNS = INITIAL_COL;
/*
The size of each rectangle, which will change according to the number of lines and columns
*/
float _W;
float _H;
/*
The higest level which someone could reach
*/
int MAX_LEVEL = 20;
/*
Level which will be increased as soon as the user reaches the top
*/
public IntegerProperty level;
/*
The current line which the rectangle will be moving
*/
int MOVING_LINE;
/*
The direction of the rectangle movement
*/
int direction = 1;
/*
The matrix of our "squares"(actually rectangles) which size is dinamic
*/
boolean[][] squares;
boolean mousePressed = false;
Random random;
StackerGame(float w, float h, GraphicsContext _gc) {
super(w, h, _gc);
_gc.getCanvas().setOnMousePressed(e -> {
mousePressed = true;
});
gameOver = new SimpleBooleanProperty(false);
score = new SimpleIntegerProperty(0);
level = new SimpleIntegerProperty(1);
random = new Random();
updateMatrix();
}
@Override
public void update() {
if (gameOver.get()) {
if (mousePressed) {
mousePressed = false;
restart();
}
return;
}
int curPos;
// update the square's position
// we will not always move the square, the pos update will be faster according to how close user gets to the top
int levelIncrease = level.get();
levelIncrease += 1 + _LINES - MOVING_LINE;
levelIncrease = constrain(levelIncrease, 0, MAX_LEVEL);
int rate = (int) map(levelIncrease, 1, MAX_LEVEL, engine.getFrameRate(), engine.getFrameRate() / 20);
boolean updatePos = engine.getFrameCount() % rate == 0;
for (curPos = 0; curPos < _COLUMNS && updatePos; curPos++) {
if (squares[curPos][MOVING_LINE]) {
if (curPos == 0) {
direction = 1;
} else if (curPos == _COLUMNS - 1) {
direction = -1;
}
// update the square matrix
squares[curPos][MOVING_LINE] = false;
if (_COLUMNS != 1) {
squares[curPos + direction][MOVING_LINE] = true;
}
break;
}
}
// if user press a key, move to the line above
if (mousePressed) {
checkStack();
if (MOVING_LINE == 0) {
level.set(level.get() + 1);
updateMatrix();
} else {
MOVING_LINE--;
squares[(int) random.nextInt(_COLUMNS - 1)][MOVING_LINE] = true;
}
mousePressed = false;
}
}
void drawSquares() {
for (int x = 0; x < _COLUMNS; x++) {
for (int y = 0; y < _LINES; y++) {
if (squares[x][y]) {
gc.setFill(Color.RED);
gc.fillRect(x * _W, y * _H, _W, _H);
if (y != _LINES - 1 && y != MOVING_LINE) {
int leftColumn = x == 0 ? -1 : x - 1;
int rightColumn = (x == _COLUMNS - 1) ? -1 : x + 1;
int line = y + 1;
if (leftColumn != -1 || rightColumn != -1) {
gc.setFill(Color.color(1, 0, 0, 0.5));
gc.fillRect(x * _W, line * _H, _W, _H);
}
}
}
}
}
}
@Override
public void display() {
gc.setFill(Color.WHITE);
gc.fillRect(0, 0, MAX_W, MAX_H);
drawGrid();
drawSquares();
}
void drawGrid() {
gc.setStroke(Color.gray(0, 0.2));
for (int x = 0; x < _COLUMNS; x++) {
for (int y = 0; y < _LINES; y++) {
gc.strokeRect(x * _W, y * _H, _W, _H);
}
}
}
private void updateMatrix() {
_LINES = level.get() * 2;
_COLUMNS += level.get() % 3 == 0 ? 1 : 0;
squares = new boolean[_COLUMNS][_LINES];
squares[0][_LINES - 1] = true;
MOVING_LINE = _LINES - 1;
_W = MAX_W / _COLUMNS;
_H = MAX_H / _LINES;
}
void checkStack() {
// no need to check at the first line
if (MOVING_LINE == _LINES - 1) {
return;
}
for (int i = 0; i < _COLUMNS; i++) {
if (squares[i][MOVING_LINE]) {
int lineToCheck = MOVING_LINE + 1;
int leftColumn = i == 0 ? -1 : i - 1;
int rightColumn = (i == _COLUMNS - 1) ? -1 : i + 1;
// perfect stack, highest score
if (squares[i][lineToCheck]) {
score.set(score.get() + MAX_POINTS);
} else if ((leftColumn != -1 && squares[leftColumn][lineToCheck])
|| (rightColumn != -1 && squares[rightColumn][lineToCheck])) {
score.set(score.get() + MIN_POINTS);
} else {
gameOver.setValue(true);
}
}
}
}
public void restart() {
level.set(1);
score.set(0);
gameOver.setValue(false);
_COLUMNS = INITIAL_COL;
_LINES = INITIAL_LINES;
updateMatrix();
}
/*
Utility methods from Processing
*/
int constrain(int v, int min, int max) {
if (v < min) {
return min;
}
if (v > max) {
return max;
} else {
return v;
}
}
private float map(float value,
float start1, float stop1,
float start2, float stop2) {
return start2 + (stop2 - start2) * ((value - start1) / (stop1 - start1));
}
}
Por fim, veja o jogo em ação:

Nenhum comentário:

Postar um comentário