"Fifteen" em Java - como desenvolver um jogo completo



Fifteen, ou Fifteen, é um ótimo exemplo de um simples jogo de lógica popular em todo o mundo. Para resolver o quebra-cabeça, você precisa organizar os quadrados com os números em ordem, do menor para o maior. Não é fácil, mas interessante.

No tutorial de hoje, mostramos como desenvolver o Fifteen no Java 8 com Eclipse. Para desenvolver a interface do usuário, usaremos a API Swing.

Lembramos que: para todos os leitores de "Habr" - um desconto de 10.000 rublos ao se inscrever em qualquer curso Skillbox usando o código promocional "Habr".

A Skillbox recomenda: O curso educacional on-line "Profissão Java-developer" .

Design de jogos


Nesta fase, você precisa definir as propriedades:
  • Tamanho - o tamanho do campo de jogo;
  • nbTiles - o número de pontos no campo. nbTiles = tamanho * tamanho - 1;
  • Tiles é uma tag, que é uma matriz unidimensional de números inteiros. Cada tag receberá um valor exclusivo no intervalo [0, nbTiles]. Zero indica um quadrado vazio;
  • blankPos - posição do quadrado vazio.

Lógica do jogo


Você precisa definir um método de redefinição usado para inicializar uma nova posição do jogo. Então, definimos o valor para cada elemento da matriz de tags. Bem, então colocamos blankPos na última posição da matriz.

Você também precisa do método de reprodução aleatória para embaralhar uma matriz de tags. Não incluímos uma tag vazia no processo de reprodução aleatória para deixá-la em sua posição anterior.

Como apenas metade das posições iniciais possíveis do quebra-cabeça tem uma solução, é necessário verificar o resultado resultante da mistura para garantir que o layout atual seja geralmente resolvido. Para fazer isso, definimos o método isSolvable.

Se uma tag específica é precedida por uma tag com um valor mais alto, isso é considerado uma inversão. Quando a etiqueta vazia está no lugar, o número de inversões deve ser uniforme para que o quebra-cabeça seja solucionável. Então, contamos o número de inversões e retornamos true se o número for par.

É importante definir o método isSolved para verificar se o alinhamento do jogo dos quinze está resolvido. Primeiro, examinamos onde está a tag vazia. Se na posição inicial, o alinhamento atual é um novo, que não foi decidido antes. Depois, iteramos sobre os blocos na ordem inversa e, se o valor da tag diferir do índice +1 correspondente, retorne false. Caso contrário, é hora de retornar true no final do método, porque o quebra-cabeça já foi resolvido.

Outro método para definir é newGame. É necessário criar uma nova instância do jogo. Para fazer isso, redefinimos o campo de jogo, embaralhá-lo e continuar até a posição de jogo ser resolvida.

Aqui está um código de exemplo com lógica de tag key:
private void newGame() { do { reset(); // reset in initial state shuffle(); // shuffle } while(!isSolvable()); // make it until grid be solvable gameOver = false; } private void reset() { for (int i = 0; i < tiles.length; i++) { tiles[i] = (i + 1) % tiles.length; } // we set blank cell at the last blankPos = tiles.length - 1; } private void shuffle() { // don't include the blank tile in the shuffle, leave in the solved position int n = nbTiles; while (n > 1) { int r = RANDOM.nextInt(n--); int tmp = tiles[r]; tiles[r] = tiles[n]; tiles[n] = tmp; } } // Only half permutations of the puzzle are solvable/ // Whenever a tile is preceded by a tile with higher value it counts // as an inversion. In our case, with the blank tile in the solved position, // the number of inversions must be even for the puzzle to be solvable private boolean isSolvable() { int countInversions = 0; for (int i = 0; i < nbTiles; i++) { for (int j = 0; j < i; j++) { if (tiles[j] > tiles[i]) countInversions++; } } return countInversions % 2 == 0; } private boolean isSolved() { if (tiles[tiles.length - 1] != 0) // if blank tile is not in the solved position ==> not solved return false; for (int i = nbTiles - 1; i >= 0; i--) { if (tiles[i] != i + 1) return false; } return true; } 

Finalmente, você precisa programar o movimento dos pontos na matriz. Esse código será chamado posteriormente por meio de um retorno de chamada para responder ao movimento do cursor. Nosso jogo suportará vários movimentos de peças simultaneamente. Assim, depois de convertermos a posição pressionada na tela em uma etiqueta, obtemos a posição de uma etiqueta vazia e procuramos a direção do movimento para suportar vários de seus movimentos ao mesmo tempo.

Aqui está um código de exemplo:
 // get position of the click int ex = e.getX() - margin; int ey = e.getY() - margin; // click in the grid ? if (ex < 0 || ex > gridSize || ey < 0 || ey > gridSize) return; // get position in the grid int c1 = ex / tileSize; int r1 = ey / tileSize; // get position of the blank cell int c2 = blankPos % size; int r2 = blankPos / size; // we convert in the 1D coord int clickPos = r1 * size + c1; int dir = 0; // we search direction for multiple tile moves at once if (c1 == c2 && Math.abs(r1 - r2) > 0) dir = (r1 - r2) > 0 ? size : -size; else if (r1 == r2 && Math.abs(c1 - c2) > 0) dir = (c1 - c2) > 0 ? 1 : -1; if (dir != 0) { // we move tiles in the direction do { int newBlankPos = blankPos + dir; tiles[blankPos] = tiles[newBlankPos]; blankPos = newBlankPos; } while(blankPos != clickPos); tiles[blankPos] = 0; 

Desenvolvemos interface do usuário na API Swing


É hora de fazer a interface. Primeiro, tomamos a classe Jpanel. Então desenhamos pontos no campo - para calcular os tamanhos de cada um, usaremos os dados especificados no parâmetro construtor do jogo:
 gridSize = (dim -  2 * margin); tileSize = gridSize / size; 

Margem também é um parâmetro especificado no construtor do jogo.

Agora você precisa definir o método drawGrid para desenhar a grade e os pontos na tela. Analisamos a matriz de tags e convertemos as coordenadas nas coordenadas da interface do usuário. Em seguida, desenhe cada etiqueta com o número correspondente no centro:
 private void drawGrid(Graphics2D g) { for (int i = 0; i < tiles.length; i++) { // we convert 1D coords to 2D coords given the size of the 2D Array int r = i / size; int c = i % size; // we convert in coords on the UI int x = margin + c * tileSize; int y = margin + r * tileSize; // check special case for blank tile if(tiles[i] == 0) { if (gameOver) { g.setColor(FOREGROUND_COLOR); drawCenteredString(g, "\u2713", x, y); } continue; } // for other tiles g.setColor(getForeground()); g.fillRoundRect(x, y, tileSize, tileSize, 25, 25); g.setColor(Color.BLACK); g.drawRoundRect(x, y, tileSize, tileSize, 25, 25); g.setColor(Color.WHITE); drawCenteredString(g, String.valueOf(tiles[i]), x , y); } } 

Finalmente, redefinimos o método paintComponent, que é um derivado da classe JPane. Em seguida, usamos o método drawGrid e, depois disso, usamos o método drawStartMessage para exibir uma mensagem solicitando o clique para iniciar o jogo:
 private void drawStartMessage(Graphics2D g) { if (gameOver) { g.setFont(getFont().deriveFont(Font.BOLD, 18)); g.setColor(FOREGROUND_COLOR); String s = "Click to start new game"; g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2, getHeight() - margin); } } private void drawCenteredString(Graphics2D g, String s, int x, int y) { // center string s for the given tile (x,y) FontMetrics fm = g.getFontMetrics(); int asc = fm.getAscent(); int desc = fm.getDescent(); g.drawString(s, x + (tileSize - fm.stringWidth(s)) / 2, y + (asc + (tileSize - (asc + desc)) / 2)); } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2D = (Graphics2D) g; g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); drawGrid(g2D); drawStartMessage(g2D); } 

Respondemos às ações do usuário na interface do usuário


Para que o jogo siga seu curso, é necessário processar as ações do usuário na interface do usuário. Para fazer isso, adicione a implementação MouseListener no Jpanel e o código para mover os pontos já mostrados acima:
 addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { // used to let users to interact on the grid by clicking // it's time to implement interaction with users to move tiles to solve the game ! if (gameOver) { newGame(); } else { // get position of the click int ex = e.getX() - margin; int ey = e.getY() - margin; // click in the grid ? if (ex < 0 || ex > gridSize || ey < 0 || ey > gridSize) return; // get position in the grid int c1 = ex / tileSize; int r1 = ey / tileSize; // get position of the blank cell int c2 = blankPos % size; int r2 = blankPos / size; // we convert in the 1D coord int clickPos = r1 * size + c1; int dir = 0; // we search direction for multiple tile moves at once if (c1 == c2 && Math.abs(r1 - r2) > 0) dir = (r1 - r2) > 0 ? size : -size; else if (r1 == r2 && Math.abs(c1 - c2) > 0) dir = (c1 - c2) > 0 ? 1 : -1; if (dir != 0) { // we move tiles in the direction do { int newBlankPos = blankPos + dir; tiles[blankPos] = tiles[newBlankPos]; blankPos = newBlankPos; } while(blankPos != clickPos); tiles[blankPos] = 0; } // we check if game is solved gameOver = isSolved(); } // we repaint panel repaint(); } }); 

Colocamos o código no construtor da classe GameOfFifteen. No final, chamamos o método newGame para iniciar um novo jogo.

Código do jogo completo


O último passo, antes de você ver o jogo em ação, é coletar todos os elementos do código juntos. Aqui está o resultado:
 import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.Random; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.SwingUtilities; // We are going to create a Game of 15 Puzzle with Java 8 and Swing // If you have some questions, feel free to read comments ;) public class GameOfFifteen extends JPanel { // our grid will be drawn in a dedicated Panel // Size of our Game of Fifteen instance private int size; // Number of tiles private int nbTiles; // Grid UI Dimension private int dimension; // Foreground Color private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // we use arbitrary color // Random object to shuffle tiles private static final Random RANDOM = new Random(); // Storing the tiles in a 1D Array of integers private int[] tiles; // Size of tile on UI private int tileSize; // Position of the blank tile private int blankPos; // Margin for the grid on the frame private int margin; // Grid UI Size private int gridSize; private boolean gameOver; // true if game over, false otherwise public GameOfFifteen(int size, int dim, int mar) { this.size = size; dimension = dim; margin = mar; // init tiles nbTiles = size * size - 1; // -1 because we don't count blank tile tiles = new int[size * size]; // calculate grid size and tile size gridSize = (dim - 2 * margin); tileSize = gridSize / size; setPreferredSize(new Dimension(dimension, dimension + margin)); setBackground(Color.WHITE); setForeground(FOREGROUND_COLOR); setFont(new Font("SansSerif", Font.BOLD, 60)); gameOver = true; addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { // used to let users to interact on the grid by clicking // it's time to implement interaction with users to move tiles to solve the game ! if (gameOver) { newGame(); } else { // get position of the click int ex = e.getX() - margin; int ey = e.getY() - margin; // click in the grid ? if (ex < 0 || ex > gridSize || ey < 0 || ey > gridSize) return; // get position in the grid int c1 = ex / tileSize; int r1 = ey / tileSize; // get position of the blank cell int c2 = blankPos % size; int r2 = blankPos / size; // we convert in the 1D coord int clickPos = r1 * size + c1; int dir = 0; // we search direction for multiple tile moves at once if (c1 == c2 && Math.abs(r1 - r2) > 0) dir = (r1 - r2) > 0 ? size : -size; else if (r1 == r2 && Math.abs(c1 - c2) > 0) dir = (c1 - c2) > 0 ? 1 : -1; if (dir != 0) { // we move tiles in the direction do { int newBlankPos = blankPos + dir; tiles[blankPos] = tiles[newBlankPos]; blankPos = newBlankPos; } while(blankPos != clickPos); tiles[blankPos] = 0; } // we check if game is solved gameOver = isSolved(); } // we repaint panel repaint(); } }); newGame(); } private void newGame() { do { reset(); // reset in intial state shuffle(); // shuffle } while(!isSolvable()); // make it until grid be solvable gameOver = false; } private void reset() { for (int i = 0; i < tiles.length; i++) { tiles[i] = (i + 1) % tiles.length; } // we set blank cell at the last blankPos = tiles.length - 1; } private void shuffle() { // don't include the blank tile in the shuffle, leave in the solved position int n = nbTiles; while (n > 1) { int r = RANDOM.nextInt(n--); int tmp = tiles[r]; tiles[r] = tiles[n]; tiles[n] = tmp; } } // Only half permutations of the puzzle are solvable. // Whenever a tile is preceded by a tile with higher value it counts // as an inversion. In our case, with the blank tile in the solved position, // the number of inversions must be even for the puzzle to be solvable private boolean isSolvable() { int countInversions = 0; for (int i = 0; i < nbTiles; i++) { for (int j = 0; j < i; j++) { if (tiles[j] > tiles[i]) countInversions++; } } return countInversions % 2 == 0; } private boolean isSolved() { if (tiles[tiles.length - 1] != 0) // if blank tile is not in the solved position ==> not solved return false; for (int i = nbTiles - 1; i >= 0; i--) { if (tiles[i] != i + 1) return false; } return true; } private void drawGrid(Graphics2D g) { for (int i = 0; i < tiles.length; i++) { // we convert 1D coords to 2D coords given the size of the 2D Array int r = i / size; int c = i % size; // we convert in coords on the UI int x = margin + c * tileSize; int y = margin + r * tileSize; // check special case for blank tile if(tiles[i] == 0) { if (gameOver) { g.setColor(FOREGROUND_COLOR); drawCenteredString(g, "\u2713", x, y); } continue; } // for other tiles g.setColor(getForeground()); g.fillRoundRect(x, y, tileSize, tileSize, 25, 25); g.setColor(Color.BLACK); g.drawRoundRect(x, y, tileSize, tileSize, 25, 25); g.setColor(Color.WHITE); drawCenteredString(g, String.valueOf(tiles[i]), x , y); } } private void drawStartMessage(Graphics2D g) { if (gameOver) { g.setFont(getFont().deriveFont(Font.BOLD, 18)); g.setColor(FOREGROUND_COLOR); String s = "Click to start new game"; g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2, getHeight() - margin); } } private void drawCenteredString(Graphics2D g, String s, int x, int y) { // center string s for the given tile (x,y) FontMetrics fm = g.getFontMetrics(); int asc = fm.getAscent(); int desc = fm.getDescent(); g.drawString(s, x + (tileSize - fm.stringWidth(s)) / 2, y + (asc + (tileSize - (asc + desc)) / 2)); } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2D = (Graphics2D) g; g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); drawGrid(g2D); drawStartMessage(g2D); } public static void main(String[] args) { SwingUtilities.invokeLater(() -> { JFrame frame = new JFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setTitle("Game of Fifteen"); frame.setResizable(false); frame.add(new GameOfFifteen(4, 550, 30), BorderLayout.CENTER); frame.pack(); // center on the screen frame.setLocationRelativeTo(null); frame.setVisible(true); }); } } 

Finalmente, jogue!


É hora de iniciar o jogo e verificá-lo em ação. O campo deve ficar assim:



Tentando resolver o quebra-cabeça. Se tudo correu bem, obtemos o seguinte:



Isso é tudo. Você esperava mais? :)

A Skillbox recomenda:

Source: https://habr.com/ru/post/pt445758/


All Articles