A „Játék” Java nyelven – teljes értékű játék fejlesztésének lépései

A „Játék” Java nyelven – teljes értékű játék fejlesztésének lépései

A Tizenöt, vagy Tizenhatos — egy nagyszerű példa egy egyszerű logikai játékra, amely világszerte népszerű. A rejtvény megoldásához a számokkal ellátott négyzeteket kell a megfelelő sorrendbe állítani, a kisebbektől a nagyobbakig. Ez nem könnyű, de izgalmas.

A mai útmutatóban bemutatjuk, hogyan fejlesszük a Tizenhatost Java 8-ban az Eclipse segítségével. A felhasználói felület fejlesztéséhez a Swing API-t használjuk.

Emlékeztetjük: minden «Habr» olvasónak 10 000 rubeles kedvezmény jár bármely Skillbox kurzus jelentkezésekor a «Habr» promóciós kód felhasználásával.

A Skillbox ajánlja: Oktatási online kurzus "Java-Fejlesztő Mesterség".

A játék tervezése

Ezen a szakaszon meg kell határozni a tulajdonságokat:

  • Size — a játéktér mérete;
  • nbTiles — a mezőben lévő tizenhatosok száma. nbTiles = size*size — 1;
  • Tiles — a tizenhat, amely egy egyirányú tömb egész számokból áll. Minden tizenhat egyedi értéket fog kapni a [0, nbTiles] tartományban. A nulla a kihagyott négyzetet jelöli;
  • blankPos — a kihagyott négyzet pozíciója.

A játék logikája

Meg kell határozni egy reset metódust, amelyet az új játékkonfiguráció inicializálására használunk. Így beállítjuk az értéket a tizenhatok tömbjének minden elemére. Ezt követően a blankPos-t a tömb utolsó pozíciójába helyezzük.

Szükség van egy shuffle metódusra is a tizenhatok tömbjének keverésére. A folyamatba nem vonjuk be a kihagyott tizenhatot, hogy az a korábbi állapotában maradhasson.

Mivel a rejtvény lehetséges kezdő pozícióinak csak a fele rendelkezik megoldással, ellenőrizni kell a kapott keverési eredményt, hogy megbizonyosodjunk arról, hogy a jelenlegi elrendezés egyáltalán megoldható. Ezt úgy tesszük, hogy meghatározzuk az isSolvable metódust.

Ha egy adott tizenhatot egy magasabb értékű tizenhat előz meg, azt inverziónak tekintjük. Amikor a kihagyott tizenhat a helyén van, az inverziók számának párosnak kell lennie ahhoz, hogy a rejtvény megoldható legyen. Így megszámoljuk az inverziók számát, és true-t adunk vissza, ha a szám páros.

Fontos meghatározni az isSolved metódust is, hogy ellenőrizzük, hogy a Game Of Fifteen elrendezésünk megoldott-e. Először nézzük, hol van a kihagyott tizenhat. Ha az eredeti pozícióban van, akkor a jelenlegi elrendezés új, korábban még nem megoldott. Ezután visszafelé végigmegyünk a csempéken, és ha a tizenhat értéke eltér a megfelelő index +1-től, false-t adunk vissza. Ellenkező esetben a metódus végén elérkezett az idő, hogy visszaadjuk a true-t, mert a rejtvény már meg van oldva.

Egy másik módszert kell meghatározni, ez pedig a newGame. Erre van szükség egy új játékpéldány létrehozásához. Ehhez visszaállítjuk a játéktáblát, majd megkeverjük, és folytatjuk, amíg a játékhelyzet megoldható nem lesz.

Itt egy példa a 15-ös logikájára vonatkozó kódra:

private void newGame() {
  do {
    reset(); // visszaállítás az alapállapotba
    shuffle(); // keverés
  } while(!isSolvable()); // addig, amíg a táblázat megoldható nem lesz
 
  gameOver = false;
}
 
private void reset() {
  for (int i = 0; i  1) {
    int r = RANDOM.nextInt(n--);
    int tmp = tiles[r];
    tiles[r] = tiles[n];
    tiles[n] = tmp;
  }
}
 
// Csak a rejtvény felének permutációi megoldhatóak
// Amikor egy lapka előtt egy magasabb értékű lapka áll, az számít egy inverziónak.
// Az esetünkben, ha a fehér lapka a megoldott helyzetben van,
// akkor az inverziók számának párosnak kell lennie ahhoz, hogy a rejtvény megoldható legyen
private boolean isSolvable() {
  int countInversions = 0;
 
  for (int i = 0; i < nbTiles; i++) {
    for (int j = 0; j  tiles[i])
        countInversions++;
    }
  }
 
  return countInversions % 2 == 0;
}
 
private boolean isSolved() {
  if (tiles[tiles.length - 1] != 0) // ha a fehér lapka nem a megoldott helyzetben van == nem megoldva
    return false;
 
  for (int i = nbTiles - 1; i >= 0; i--) {
    if (tiles[i] != i + 1)
      return false;
  }
 
  return true;
}

Végül programozni kell a 15-ös mozgását a tömbben. Ezt a kódot később egy visszahívás (callback) révén hívjuk meg, hogy reagáljon a kurzor mozgására. A mi játékunk támogatni fogja a lapkák több mozgását egyidejűleg. Így miután átfordítottuk a képernyőn megnyomott pozíciót egy 15-ös lapkára, megkapjuk a fehér 15-ös pozícióját, és megkeressük a mozgás irányát a több mozgás támogatásához.

Itt egy példa a kódra:

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

UI fejlesztése Swing API-val

Itt az ideje foglalkozni a felülettel. Először is a Jpanel osztályt vesszük. Majd a táblára rajzoljuk a 15-ös lapkákat - minden egyes méret kiszámításához a játék konstruktorának paramétereiben megadott adatokra támaszkodunk:

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

A margin is egy paraméter, amelyet a játék konstruktora határoz meg.

Most meg kell határoznunk a drawGrid metódust a hálózat és a 15-ös lapkák képernyőre rajzolásához. Elemezzük a 15-ös tömböt, és konvertáljuk a koordinátákat a felhasználói felület koordinátáira. Ezután minden egyes 15-öst a megfelelő számmal az anyag közepére rajzolunk:

private void drawGrid(Graphics2D g) {
  for (int i = 0; i < tiles.length; i++) {
    // a 1D koordinátákat 2D koordinátákra alakítjuk a 2D tömb mérete alapján
    int r = i / size;
    int c = i % size;
    // UI koordinátákra alakítjuk
    int x = margin + c * tileSize;
    int y = margin + r * tileSize;

    // különleges eset a üres csempe
    if(tiles[i] == 0) {
      if (gameOver) {
        g.setColor(FOREGROUND_COLOR);
        drawCenteredString(g, "u2713", x, y);
      }

      continue;
    }

    // más csempék esetén
    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);
  }
}

Végül felülírjuk a paintComponent metódust, ami a JPane osztály származéka. Ezután használjuk a drawGrid metódust, majd a drawStartMessage metódust a játék indítására vonatkozó üzenet megjelenítéséhez:

private void drawStartMessage(Graphics2D g) {
  if (gameOver) {
    g.setFont(getFont().deriveFont(Font.BOLD, 18));
    g.setColor(FOREGROUND_COLOR);
    String s = "Kattints az új játék indításához";
    g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
        getHeight() - margin);
  }
}

private void drawCenteredString(Graphics2D g, String s, int x, int y) {
  // a string s középre igazítása az adott csempe (x,y) alapján
  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);
}

Reagálunk a felhasználó tevékenységeire az UI-ban

Azért, hogy a játék gördülékenyen haladhasson, kezelni kell a felhasználói tevékenységeket az UI-ban. Ehhez hozzáadunk egy MouseListener implementációt a JPanel-hez, valamint a csempék mozgatásához szükséges kódot, amit már korábban bemutattunk:

addMouseListener(new MouseAdapter() {
  @Override
  public void mousePressed(MouseEvent e) {
    // a felhasználók interakciós lehetőségei a rácson való kattintás által
    // itt az ideje implementálni a felhasználói interakciót a csempék mozgatásához a játék megoldásához!
    if (gameOver) {
      newGame();
    } else {
      // a kattintás pozíciójának megkérése
      int ex = e.getX() - margin;
      int ey = e.getY() - margin;

      // kattintás a rácson?
      if (ex  gridSize || ey  gridSize)
        return;

      // pozíció a rácsban
      int c1 = ex / tileSize;
      int r1 = ey / tileSize;

      // az üres cella pozíciója
      int c2 = blankPos % size;
      int r2 = blankPos / size;

      // 1D koordinátára alakítjuk
      int clickPos = r1 * size + c1;

      int dir = 0;

      // az irány keresése több csempe egyszerre történő mozgatásához
      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) {
        // a csempék mozgatása az irányban
        do {
          int newBlankPos = blankPos + dir;
          tiles[blankPos] = tiles[newBlankPos];
          blankPos = newBlankPos;
        } while(blankPos != clickPos);

        tiles[blankPos] = 0;
      }

      // ellenőrizzük, hogy a játék megoldódott-e
      gameOver = isSolved();
    }

    // újrarajzoljuk a panelt
    repaint();
  }
});

A kódot a GameOfFifteen osztály készítőjében helyezzük el. A legvégén a newGame metódust hívjuk meg egy új játék elkezdéséhez.

A játék teljes kódja

Az utolsó lépés, mielőtt látnánk a játék működését, hogy az összes kódelem össze legyen gyűjtve. Íme, mi fog kisülni:

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;
 
// A 15 Puzzle játékot fogunk létrehozni Java 8 és Swing segítségével
// Ha kérdéseid vannak, nyugodtan olvasd el a megjegyzéseket ;)
public class GameOfFifteen extends JPanel { // A rácsunkat egy dedikált panelen fogjuk megrajzolni
 
  // A 15-ös játékunk példányának mérete
  private int size;
  // Csempe szám
  private int nbTiles;
  // Rács UI dimenzió
  private int dimension;
  // Előtér szín
  private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // tetszőleges színt használunk
  // Véletlenszerű objektum a csempék megkeveréséhez
  private static final Random RANDOM = new Random();
  // Csempék tárolása egy 1D egész számú tömbben
  private int[] tiles;
  // Csempe mérete a felhasználói felületen
  private int tileSize;
  // A üres csempe pozíciója
  private int blankPos;
  // Margó a rács számára a kereten
  private int margin;
  // Rács UI mérete
  private int gridSize;
  private boolean gameOver; // true, ha a játék véget ért, false egyébként
 
  public GameOfFifteen(int size, int dim, int mar) {
    this.size = size;
    dimension = dim;
    margin = mar;
    
    // csempék inicializálása
    nbTiles = size * size - 1; // -1 mert az üres csempét nem számítjuk
    tiles = new int[size * size];
    
    // rács méretének és csempe méretének kiszámítása
    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) {
        // Használják, hogy a felhasználók interakcióba lépjenek a rácsra kattintva
        // itt az ideje implementálni az interakciót, hogy a csempéket mozgatni lehessen a játék megoldásához!
        if (gameOver) {
          newGame();
        } else {
          // a kattintás pozíciójának lekérdezése
          int ex = e.getX() - margin;
          int ey = e.getY() - margin;
          
          // kattintás a rácsban?
          if (ex  gridSize || ey  gridSize)
            return;
          
          // pozíció a rácsban
          int c1 = ex / tileSize;
          int r1 = ey / tileSize;
          
          // a üres cella pozíciója
          int c2 = blankPos % size;
          int r2 = blankPos / size;
          
          // 1D koordinátába való konvertálás
          int clickPos = r1 * size + c1;
          
          int dir = 0;
          
          // irány keresése több csempe mozgásához egyszerre
          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) {
            // csempék mozgatása a megadott irányba
            do {
              int newBlankPos = blankPos + dir;
              tiles[blankPos] = tiles[newBlankPos];
              blankPos = newBlankPos;
            } while(blankPos != clickPos);
            
            tiles[blankPos] = 0;
          }
          
          // ellenőrizzük, hogy a játék megoldódott-e
          gameOver = isSolved();
        }
        
        // újrarajzoljuk a panelt
        repaint();
      }
    });
    
    newGame();
  }
 
  private void newGame() {
    do {
      reset(); // visszaállítás kezdeti állapotba
      shuffle(); // keverés
    } while(!isSolvable()); // addig, amíg megoldható nem lesz a rács
    
    gameOver = false;
  }
 
  private void reset() {
    for (int i = 0; i  1) {
      int r = RANDOM.nextInt(n--);
      int tmp = tiles[r];
      tiles[r] = tiles[n];
      tiles[n] = tmp;
    }
  }
 
  // Csak a puzzle felének permutációi oldhatók meg.
  // Amikor egy csempe egy nagyobb értékű csempe előtt áll, az számít
  // mint egy inverzió. A mi esetünkben, az üres csempe megoldott állapotban,
  // a fordítottak számának párosnak kell lennie a puzzle megoldásához
  private boolean isSolvable() {
    int countInversions = 0;
    
    for (int i = 0; i < nbTiles; i++) {
      for (int j = 0; j  tiles[i])
          countInversions++;
      }
    }
    
    return countInversions % 2 == 0;
  }
 
  private boolean isSolved() {
    if (tiles[tiles.length - 1] != 0) // ha az üres csempe nem a megoldott állapotban van => nem megoldott
      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  {
      JFrame frame = new JFrame();
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      frame.setTitle("Tizenötödik Játék");
      frame.setResizable(false);
      frame.add(new GameOfFifteen(4, 550, 30), BorderLayout.CENTER);
      frame.pack();
      // középre igazítani a képernyőn
      frame.setLocationRelativeTo(null);
      frame.setVisible(true);
    });
  }
 
 
}

Végre játszunk!

Itt az ideje, hogy elindítsuk a játékot és ellenőrizzük, hogyan működik. A térképnek a következőképpen kell kinéznie:

A „Játék” Java nyelven – teljes értékű játék fejlesztésének lépései

Próbálkozunk a rejtvény megoldásával. Ha minden rendben ment, ezt kapjuk:

A „Játék” Java nyelven – teljes értékű játék fejlesztésének lépései

Ennyi volt. Többre számítottál? 🙂

A Skillbox ajánlja:

Forrás: habr.com

Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster