«Femten» pĂ„ Java – hvordan utvikle et fullt spill

«Femten» pĂ„ Java – hvordan utvikle et fullt spill

«Femten», eller «Femten-spillet» — et utmerket eksempel pĂ„ et enkelt logikkspill som er populĂŠrt over hele verden. For Ă„ lĂžse puslespillet mĂ„ kvadratene med tall ordnes i rekkefĂžlge, fra laveste til hĂžyeste. Dette er ikke enkelt, men interessant.

I dagens tutorial viser vi hvordan man kan utvikle «Femten» pÄ Java 8 med Eclipse. For Ä utvikle brukergrensesnittet vil vi bruke Swing API.

Vi minner om: for alle lesere av «Habr» — 10 000 rubler rabatt ved pĂ„melding til hvilket som helst Skillbox-kurs med kampanjekoden «Habr».

Skillbox anbefaler: Nettbasert utdanningskurs «Yrkesfag Java-utvikler».

Design av spillet

I dette trinnet mÄ vi bestemme egenskapene:

  • Size — stĂžrrelsen pĂ„ spillefeltet;
  • nbTiles — antall brikker i feltet. nbTiles = size*size — 1;
  • Tiles — brikker som representerer et endimensjonalt array av heltall. Hver brikke vil fĂ„ en unik verdi innenfor intervallet [0, nbTiles]. Null representerer et tomt kvadrat;
  • blankPos — posisjonen til det tomme kvadratet.

Logikken til spillet

Vi mÄ definere en reset-metode som brukes til Ä initialisere en ny spillposisjon. Vi setter verdien for hvert element i brikkearrayet. Deretter plasserer vi blankPos i den siste posisjonen i arrayet.

Vi trenger ogsÄ en shuffle-metode for Ä stokke brikkene. Vi inkluderer ikke den tomme brikken i stokkingprosessen for Ä la den vÊre pÄ sin opprinnelige plass.

Siden bare halvparten av de mulige startposisjonene for puslespillet har en lÞsning, mÄ vi sjekke det resulterende blandingsresultatet for Ä forsikre oss om at den nÄvÊrende oppstillingen faktisk er lÞselig. For Ä gjÞre dette definerer vi en isSolvable-metode.

Hvis en bestemt brikke er foran en brikke med hÞyere verdi, regnes dette som en inversjon. NÄr den tomme brikken er pÄ sin plass, mÄ antallet inversjoner vÊre partall for at puslespillet skal vÊre lÞselig. SÄ vi teller antall inversjoner og returnerer true hvis tallet er partall.

Deretter er det viktig Ä definere en isSolved-metode for Ä sjekke om vÄr oppstilling av Game Of Fifteen er lÞst. FÞrst ser vi hvor den tomme brikken er. Hvis den er i startposisjonen, er den nÄvÊrende oppstillingen ny og har ikke vÊrt lÞst tidligere. Deretter gÄr vi gjennom brikkene i omvendt rekkefÞlge, og hvis verdien av brikken er forskjellig fra den tilsvarende indeksen +1, returnerer vi false. Ellers, pÄ slutten av metoden, er det pÄ tide Ä returnere true, fordi puslespillet allerede er lÞst.

En annen metode som mÄ defineres, er newGame. Denne er nÞdvendig for Ä opprette en ny instans av spillet. For dette tilbakestiller vi spillefeltet, deretter stokker vi det og fortsetter til spillposisjonen er lÞselig.

Her er et eksempel pÄ kode med nÞkkellogikken for en 15-brikker:

private void newGame() {
  do {
    reset(); // tilbakestill til opprinnelig tilstand
    shuffle(); // stokke
  } while(!isSolvable()); // fortsett til rutenettet er lĂžselig
 
  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;
  }
}
 
// Bare halvparten av permutasjonene av puslespillet er lĂžselige/
// Hver gang en brikke forekommer foran en brikke med hĂžyere verdi, teller det
// som en inversjon. I vÄrt tilfelle, med den blanke brikken i lÞst posisjon,
// mÄ antallet inversjoner vÊre jevnt for at puslespillet skal vÊre lÞselig
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) // hvis den blanke brikken ikke er i lĂžst posisjon ==> ikke lĂžst
    return false;
 
  for (int i = nbTiles - 1; i >= 0; i--) {
    if (tiles[i] != i + 1)
      return false;
  }
 
  return true;
}

Til slutt mÄ vi programmere bevegelsen av brikkene i matrisen. Denne koden vil bli kalt senere via en callback, for Ä reagere pÄ musepekeren. Spillet vÄrt vil stÞtte flere brikker som flyttes samtidig. Dermed, etter at vi har konvertert den trykkte posisjonen pÄ skjermen til en brikke, fÄr vi posisjonen til den blanke brikken og ser etter bevegelseretning for Ä stÞtte flere flyttinger samtidig.

Her er et eksempel pÄ kode:

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

Utvikler UI med Swing API

Det er pĂ„ tide Ă„ ta fatt pĂ„ grensesnittet. FĂžrst tar vi klassen Jpanel. SĂ„ tegner vi brikkene pĂ„ feltet — for Ă„ beregne stĂžrrelsen pĂ„ hver vil vi bruke dataene angitt i parameterne til spillets konstruktĂžr:

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

Margin er ogsÄ en parameter gitt i konstruktÞren til spillet.

NÄ mÄ vi definere metoden drawGrid for Ä tegne rutenettet og brikkene pÄ skjermen. Vi analyserer matrisen av brikker og konverterer koordinatene til brukergrensesnittkoordinater. Deretter tegner vi hver brikke med det tilsvarende tallet i midten:

private void drawGrid(Graphics2D g) {
  for (int i = 0; i < tiles.length; i++) {
    // vi konverterer 1D-koordinater til 2D-koordinater givet stĂžrrelsen af 2D-arrayet
    int r = i / size;
    int c = i % size;
    // vi konverterer til koordinater i UI
    int x = margin + c * tileSize;
    int y = margin + r * tileSize;
 
    // tjekker for speciale tilfĂŠlde for tom flise
    if(tiles[i] == 0) {
      if (gameOver) {
        g.setColor(FOREGROUND_COLOR);
        drawCenteredString(g, "u2713", x, y);
      }
 
      continue;
    }
 
    // for andre fliser
    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);
  }
}

Til sidst vil vi overskrive metoden paintComponent, som er afledt af klassen JPane. Derefter bruger vi metoden drawGrid, og til sidst metoden drawStartMessage for at vise en besked, der beder om at klikke for at starte spillet:

private void drawStartMessage(Graphics2D g) {
  if (gameOver) {
    g.setFont(getFont().deriveFont(Font.BOLD, 18));
    g.setColor(FOREGROUND_COLOR);
    String s = "Klik for at starte nyt spil";
    g.drawString(s, (getWidth() - g.getFontMetrics().stringWidth(s)) / 2,
        getHeight() - margin);
  }
}
 
private void drawCenteredString(Graphics2D g, String s, int x, int y) {
  // centrerer strengen s for den givne flise (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);
}

Vi reagerer pÄ brugerhandlinger i UI

For at sikre, at spillet forlÞber som det skal, er det nÞdvendigt at hÄndtere brugerhandlinger i UI. Derfor tilfÞjer vi implementeringen af MouseListener til Jpanel og koden til at flytte fliserne, som allerede er vist ovenfor:

addMouseListener(new MouseAdapter() {
  @Override
  public void mousePressed(MouseEvent e) {
    // bruges til at lade brugerne interagere pÄ nettet ved at klikke
    // det er tid til at implementere interaktion med brugerne for at flytte fliserne for at lĂžse spillet!
    if (gameOver) {
      newGame();
    } else {
      // fÄ positionen af klikket
      int ex = e.getX() - margin;
      int ey = e.getY() - margin;
 
      // klikker i nettet?
      if (ex  gridSize  || ey  gridSize)
        return;
 
      // fÄ positionen i nettet
      int c1 = ex / tileSize;
      int r1 = ey / tileSize;
 
      // fÄ positionen af den tomme celle
      int c2 = blankPos % size;
      int r2 = blankPos / size;
 
      // vi konverterer til den 1D koordinat
      int clickPos = r1 * size + c1;
 
      int dir = 0;
 
      // vi sĂžger retning for flere flisebevĂŠgelser ad gangen
      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) {
        // vi flytter fliserne i den retning
        do {
          int newBlankPos = blankPos + dir;
          tiles[blankPos] = tiles[newBlankPos];
          blankPos = newBlankPos;
        } while(blankPos != clickPos);
 
        tiles[blankPos] = 0;
      }
 
      // vi tjekker om spillet er lĂžst
      gameOver = isSolved();
    }
 
    // vi tegner panelet igen
    repaint();
  }
});

Koden plasseres i klassebyggeren GameOfFifteen. Til slutt kaller vi metoden newGame for Ă„ starte et nytt spill.

Den fullstendige koden for spillet

Det siste steget fĂžr du ser spillet i aksjon, er Ă„ samle alle kodeelementene sammen. Slik blir det:

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;
 
// Vi skal lage et 15 puslespill med Java 8 og Swing
// Hvis du har spÞrsmÄl, kan du gjerne lese kommentarene ;)
public class GameOfFifteen extends JPanel { // vÄr grid vil bli tegnet i et dedikert panel
 
  // StÞrrelse pÄ vÄr Game of Fifteen-instans
  private int size;
  // Antall brikker
  private int nbTiles;
  // Grid UI-dimensjon
  private int dimension;
  // Forgrunnsfarge
  private static final Color FOREGROUND_COLOR = new Color(239, 83, 80); // vi bruker en vilkÄrlig farge
  // Tilfeldig objekt for Ă„ stokke brikkene
  private static final Random RANDOM = new Random();
  // Lagrer brikkene i et 1D-array av heltall
  private int[] tiles;
  // StÞrrelse pÄ brikken pÄ UI
  private int tileSize;
  // Posisjon for den tomme brikken
  private int blankPos;
  // Margin for grid pÄ rammen
  private int margin;
  // Grid UI-stĂžrrelse
  private int gridSize;
  private boolean gameOver; // true hvis spillet er over, false ellers
 
  public GameOfFifteen(int size, int dim, int mar) {
    this.size = size;
    dimension = dim;
    margin = mar;
    
    // init brikker
    nbTiles = size * size - 1; // -1 fordi vi ikke teller den tomme brikken
    tiles = new int[size * size];
    
    // kalkuler grid-stĂžrrelse og brikkestĂžrrelse
    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) {
        // brukt for Ă„ la brukerne samhandle med grid ved Ă„ klikke
        // det er pÄ tide Ä implementere interaksjon med brukerne for Ä flytte brikkene for Ä lÞse spillet!
        if (gameOver) {
          newGame();
        } else {
          // fÄ posisjonen til klikket
          int ex = e.getX() - margin;
          int ey = e.getY() - margin;
          
          // klikk i grid?
          if (ex  gridSize  || ey  gridSize)
            return;
          
          // fÄ posisjonen i grid
          int c1 = ex / tileSize;
          int r1 = ey / tileSize;
          
          // fÄ posisjonen til den tomme cellen
          int c2 = blankPos % size;
          int r2 = blankPos / size;
          
          // vi konverterer til 1D koordinat
          int clickPos = r1 * size + c1;
          
          int dir = 0;
          
          // vi sÞker retningen for flere brikketransporter pÄ en gang
          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) {
            // vi flytter brikkene i retning
            do {
              int newBlankPos = blankPos + dir;
              tiles[blankPos] = tiles[newBlankPos];
              blankPos = newBlankPos;
            } while(blankPos != clickPos);
            
            tiles[blankPos] = 0;
          }
          
          // vi sjekker om spillet er lĂžst
          gameOver = isSolved();
        }
        
        // vi tegner panelet pÄ nytt
        repaint();
      }
    });
    
    newGame();
  }
 
  private void newGame() {
    do {
      reset(); // tilbakestill til opprinnelig tilstand
      shuffle(); // stokke
    } while(!isSolvable()); // gjĂžr det til grid er lĂžsbart
    
    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;
    }
  }
 
  // Bare halvparten av permutasjonene av puslespillet er lĂžsbart.
  // Hver gang en brikke er forutgÄende av en brikke med hÞyere verdi, telles det
  // som en inversjon. I vÄrt tilfelle, med den tomme brikken i lÞst posisjon,
  // mÄ antallet inversjoner vÊre jevnt for at puslespillet skal vÊre lÞsbart
  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) // hvis den tomme brikken ikke er i den lĂžste posisjonen == ikke lĂžst
      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("Game of Fifteen");
      frame.setResizable(false);
      frame.add(new GameOfFifteen(4, 550, 30), BorderLayout.CENTER);
      frame.pack();
      // sentrer pÄ skjermen
      frame.setLocationRelativeTo(null);
      frame.setVisible(true);
    });
  }
 
 
}

Endelig, la oss spille!

Det er pÄ tide Ä starte spillet og teste det i aksjon. Feltet skal se slik ut:

«Femten» pĂ„ Java – hvordan utvikle et fullt spill

La oss prÞve Ä lÞse puslespillet. Hvis alt gikk bra, fÄr vi dette:

«Femten» pĂ„ Java – hvordan utvikle et fullt spill

Og der har vi det. Forventet du mer? 🙂

Skillbox anbefaler:

Kilde: habr.com

KjĂžp pĂ„litelig hosting for nettsteder med DDoS-beskyttelse, VPS VDS-servere đŸ”„ KjĂžp pĂ„litelig hosting for nettsteder med DDoS-beskyttelse, VPS VDS-servere | ProHoster