Javaの「15」-本格的なゲームの開発方法





フィフティーン、またはフィフティーンは、世界中で人気のあるシンプルなロジックゲームの素晴らしい例です。 パズルを解くには、小さい方から大きい方へ順番に正方形を数字で並べる必要があります。 簡単ではありませんが、面白いです。



本日のチュートリアルでは、EclipseでJava 8で15を開発する方法を示します。 UIを開発するには、Swing APIを使用します。



「Habr」の読者には、「Habr」プロモーションコードを使用してSkillboxコースに登録すると10,000ルーブルの割引があります。



Skillboxの推奨事項:オンライン教育コース「Profession Java-developer」


ゲームデザイン



この段階で、プロパティを定義する必要があります。



ゲームロジック



新しいゲーム位置を初期化するために使用されるリセット方法を定義する必要があります。 そこで、タグ配列の各要素に値を設定します。 それでは、配列の最後の位置にblankPosを配置します。



タグの配列をシャッフルするには、shuffleメソッドも必要です。 シャッフルプロセスに空のタグを含めて、元の位置のままにすることはありません。



パズルの可能な開始位置の半分しか解決策がないため、結果のミキシング結果を確認して、現在のレイアウトが一般的に解決されていることを確認する必要があります。 これを行うには、isSolvableメソッドを定義します。



特定のタグの前に高い値のタグが付いている場合、これは反転と見なされます。 空のタグが配置されている場合、パズルを解くには反転の数が偶数である必要があります。 したがって、反転の数をカウントし、その数が偶数の場合はtrueを返します。



その後、isSolvedメソッドを定義して、Game Of 15のアライメントが解決されたかどうかを確認することが重要です。 最初に、空のタグの場所を調べます。 初期位置にある場合、現在のアライメントは新しいものであり、以前に決定されていません。 次に、タイルを逆の順序で繰り返し処理し、タグの値が対応するインデックス+1と異なる場合、falseを返します。 そうでなければ、パズルはすでに解決されているため、メソッドの最後にtrueを返す時間です。



定義する別のメソッドはnewGameです。 ゲームの新しいインスタンスを作成する必要があります。 これを行うには、プレイフィールドをリセットしてからシャッフルし、プレイ位置が解決可能になるまで続けます。



キータグロジックを使用したサンプルコードを次に示します。

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; }
      
      





最後に、アレイ内のスポットの動きをプログラムする必要があります。 このコードは、カーソルの移動に応答するコールバックを介して後で呼び出されます。 このゲームは、複数のタイルの移動を同時にサポートします。 したがって、画面上の押された位置をタグに変換した後、空のタグの位置を取得し、動きの方向を探してその動きのいくつかを同時にサポートします。



サンプルコードを次に示します。

 // 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;
      
      





Swing APIでUIを開発します



インターフェースを実行する時が来ました。 まず、Jpanelクラスを使用します。 次に、フィールドにスポットを描画します-それぞれのサイズを計算するために、ゲームのコンストラクターパラメーターで指定されたデータを使用します。

 gridSize = (dim -  2 * margin); tileSize = gridSize / size;
      
      





マージンは、ゲームコンストラクターで指定されるパラメーターでもあります。



次に、グリッドとスポットを画面に描画するためにdrawGridメソッドを定義する必要があります。 タグの配列を分析し、座標をユーザーインターフェイスの座標に変換します。 次に、中央に対応する番号を持つ各タグを描画します。

 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); } }
      
      





最後に、JPaneクラスの派生物であるpaintComponentメソッドを再定義します。 次にdrawGridメソッドを使用し、その後drawStartMessageメソッドを使用して、クリックしてゲームを開始するように促すメッセージを表示します。

 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); }
      
      





UIでのユーザーアクションに応答します



ゲームがコースを取るためには、UIでユーザーアクションを処理する必要があります。 これを行うには、JpanelにMouseListener実装を追加し、既に上に示したスポットを移動するためのコードを追加します。

 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(); } });
      
      





GameOfFifteenクラスのコンストラクターにコードを配置します。 最後に、newGameメソッドを呼び出して新しいゲームを開始します。



完全なゲームコード



ゲームの動作を確認する前の最後のステップは、すべてのコード要素を一緒に収集することです。 結果は次のとおりです。

 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); }); } }
      
      





最後に、プレイ!



ゲームを開始し、動作を確認する時が来ました。 フィールドは次のようになります。







パズルを解こうとしています。 すべてがうまくいけば、これが得られます:







以上です。 もっと期待しましたか? :)



Skillboxの推奨事項:






All Articles