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.

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:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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; | |
} | |
} |
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:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
} | |
} |
Nenhum comentário:
Postar um comentário