"Fünfzehn" in Java - wie man ein vollwertiges Spiel entwickelt



Fünfzehn oder fünfzehn ist ein großartiges Beispiel für ein einfaches Logikspiel, das auf der ganzen Welt beliebt ist. Um das Rätsel zu lösen, müssen Sie die Quadrate mit Zahlen in der Reihenfolge von kleiner bis größer anordnen. Es ist nicht einfach, aber interessant.

Im heutigen Tutorial zeigen wir, wie man mit Eclipse Fifteen in Java 8 entwickelt. Zur Entwicklung der Benutzeroberfläche verwenden wir die Swing-API.

Wir erinnern Sie daran: Für alle Leser von „Habr“ - ein Rabatt von 10.000 Rubel bei der Anmeldung für einen Skillbox-Kurs mit dem Promo-Code „Habr“.

Skillbox empfiehlt: Der Online-Schulungskurs "Profession Java-Entwickler" .

Spieldesign


In dieser Phase müssen Sie die Eigenschaften definieren:
  • Größe - die Größe des Spielfelds;
  • nbTiles - Die Anzahl der Punkte im Feld. nbTiles = Größe * Größe - 1;
  • Tiles ist ein Tag, bei dem es sich um ein eindimensionales Array von Ganzzahlen handelt. Jedes Tag erhält einen eindeutigen Wert im Bereich [0, nbTiles]. Null bezeichnet ein leeres Quadrat;
  • blankPos - Position des leeren Quadrats.

Spielelogik


Sie müssen eine Rücksetzmethode definieren, mit der eine neue Spielposition initialisiert wird. Also setzen wir den Wert für jedes Element des Tag-Arrays. Nun, dann setzen wir blankPos an die letzte Position des Arrays.

Sie benötigen auch die Shuffle-Methode, um ein Array von Tags zu mischen. Wir nehmen kein leeres Tag in den Mischvorgang auf, um es an seiner früheren Position zu belassen.

Da nur die Hälfte der möglichen Startpositionen des Puzzles eine Lösung hat, müssen Sie das resultierende Mischungsergebnis überprüfen, um sicherzustellen, dass das aktuelle Layout im Allgemeinen gelöst ist. Dazu definieren wir die isSolvable-Methode.

Wenn einem bestimmten Tag ein Tag mit einem höheren Wert vorangestellt ist, wird dies als Inversion betrachtet. Wenn das leere Tag vorhanden ist, muss die Anzahl der Inversionen gerade sein, damit das Rätsel lösbar ist. Wir zählen also die Anzahl der Inversionen und geben true zurück, wenn die Anzahl gerade ist.

Dann ist es wichtig, die isSolved-Methode zu definieren, um zu überprüfen, ob unsere Ausrichtung von Game Of Fifteen gelöst ist. Zuerst schauen wir uns an, wo sich das leere Tag befindet. In der Ausgangsposition ist die aktuelle Ausrichtung eine neue, die zuvor noch nicht festgelegt wurde. Dann iterieren wir in umgekehrter Reihenfolge über die Kacheln. Wenn der Tag-Wert vom entsprechenden + 1-Index abweicht, geben Sie false zurück. Andernfalls ist es an der Zeit, am Ende der Methode true zurückzugeben, da das Rätsel bereits gelöst wurde.

Eine andere zu definierende Methode ist newGame. Es ist erforderlich, eine neue Instanz des Spiels zu erstellen. Dazu setzen wir das Spielfeld zurück, mischen es dann und fahren fort, bis die Spielposition auflösbar ist.

Hier ist ein Beispielcode mit Schlüssel-Tag-Logik:
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; } 

Schließlich müssen Sie die Bewegung der Punkte im Array programmieren. Dieser Code wird später über einen Rückruf aufgerufen, um auf die Cursorbewegung zu reagieren. Unser Spiel unterstützt mehrere Kachelbewegungen gleichzeitig. Nachdem wir die gedrückte Position auf dem Bildschirm in ein Tag umgewandelt haben, erhalten wir die Position eines leeren Tags und suchen nach der Bewegungsrichtung, um mehrere seiner Bewegungen gleichzeitig zu unterstützen.

Hier ist ein Beispielcode:
 // 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; 

Wir entwickeln eine Benutzeroberfläche für die Swing-API


Es ist Zeit, die Schnittstelle zu machen. Zuerst nehmen wir an der Jpanel-Klasse teil. Dann zeichnen wir Punkte auf dem Spielfeld - um die Größe der einzelnen Punkte zu berechnen, verwenden wir die Daten, die im Konstruktorparameter des Spiels angegeben sind:
 gridSize = (dim -  2 * margin); tileSize = gridSize / size; 

Margin ist auch ein im Spielkonstruktor angegebener Parameter.

Jetzt müssen Sie die drawGrid-Methode definieren, um das Raster und die Punkte auf dem Bildschirm zu zeichnen. Wir analysieren das Array von Tags und konvertieren die Koordinaten in die Koordinaten der Benutzeroberfläche. Zeichnen Sie dann jedes Tag mit der entsprechenden Nummer in der Mitte:
 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); } } 

Schließlich definieren wir die paintComponent-Methode neu, die eine Ableitung der JPane-Klasse ist. Dann verwenden wir die drawGrid-Methode und danach verwenden wir die drawStartMessage-Methode, um eine Meldung anzuzeigen, die uns auffordert, zu klicken, um das Spiel zu starten:
 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); } 

Wir reagieren auf Benutzeraktionen in der Benutzeroberfläche


Damit das Spiel seinen Lauf nimmt, müssen Benutzeraktionen in der Benutzeroberfläche verarbeitet werden. Fügen Sie dazu die MouseListener-Implementierung auf dem Jpanel und den Code zum Verschieben der oben bereits gezeigten Spots hinzu:
 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(); } }); 

Wir platzieren den Code im Konstruktor der GameOfFifteen-Klasse. Ganz am Ende rufen wir die newGame-Methode auf, um ein neues Spiel zu starten.

Vollständiger Spielcode


Der letzte Schritt, bevor Sie das Spiel in Aktion sehen, besteht darin, alle Codeelemente zusammen zu sammeln. Hier ist das Ergebnis:
 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); }); } } 

Endlich spielen!


Es ist Zeit, das Spiel zu starten und es in Aktion zu überprüfen. Das Feld sollte folgendermaßen aussehen:



Der Versuch, das Rätsel zu lösen. Wenn alles gut gegangen ist, bekommen wir folgendes:



Das ist alles. Hast du mehr erwartet? :) :)

Skillbox empfiehlt:

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


All Articles