06.03.2013
JavaFX - Game of Life
In dem folgenden Beispiel wird mit JavaFX eine einfache Oberfläche für Game of Life erstellt. Die Zellen werden mit javafx.scene.layout.StackPane
s realisiert, die innerhalb eines javafx.scene.layout.Pane
angeordnet sind. Die StackPane
-Objekte werden mittels eines javafx.scene.layout.StackPaneBuilder
erstellt. Die Anmination wurde mit einer javafx.animation.Timeline
umgesetzt.
Das fertige Spiel ist auf folgendem Screenshot zu sehen. (Zum Abspielen des Films einfach das Bild anklicken.)
Der Quellcode dazu sieht folgendermaßen aus:
package org.hameister.gol; import java.util.HashMap; import java.util.Map; import javafx.animation.KeyFrame; import javafx.animation.Timeline; import javafx.application.Application; import javafx.event.Event; import javafx.event.EventHandler; import javafx.scene.Scene; import javafx.scene.layout.Pane; import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPaneBuilder; import javafx.stage.Stage; import javafx.util.Duration; public class GameOfLife extends Application { private static final int WIDTH = 10; private static final int HEIGHT = 10; private static final int BOARD_SIZE = 320; private Map<String, StackPane> boardMap = new HashMap<>(); private Board board = new Board(BOARD_SIZE/10); @Override public void start(Stage primaryStage) { final Timeline timeline = new Timeline(new KeyFrame(Duration.ZERO, new EventHandler() { @Override public void handle(Event event) { iterateBoard(); } }), new KeyFrame(Duration.millis(100))); timeline.setCycleCount(Timeline.INDEFINITE); board.initBoard(0.5); Pane root = new Pane(); Scene scene = new Scene(root, BOARD_SIZE, BOARD_SIZE); scene.getStylesheets().add("gol.css"); // Create a board with dead cells for (int x = 0; x < BOARD_SIZE; x = x + WIDTH) { for (int y = 0; y < BOARD_SIZE; y = y + HEIGHT) { StackPane cell = StackPaneBuilder.create().layoutX(x).layoutY(y).prefHeight(HEIGHT).prefWidth(WIDTH).styleClass("dead-cell").build(); root.getChildren().add(cell); //Store the cell in a HashMap for fast access //in the iterateBoard method. boardMap.put(x + " " + y, cell); } } primaryStage.setTitle("Game of Life"); primaryStage.setScene(scene); primaryStage.show(); timeline.play(); } private void iterateBoard() { board.nextPopulation(); for (int x = 0; x < board.getSize(); x++) { for (int y = 0; y < board.getSize(); y++) { StackPane pane = boardMap.get(x * WIDTH + " " + y * HEIGHT); pane.getStyleClass().clear(); // If the cell at (x,y) is a alive use css styling 'alive-cell' // otherwise use the styling 'dead-cell'. if (board.getField(x, y) == 1) { pane.getStyleClass().add("alive-cell"); } else { pane.getStyleClass().add("dead-cell"); } } } } public static void main(String[] args) { launch(args); } }
In der Methode start
wird als erstes eine Timeline
erstellt. Sie dient dazu alle 100 Millisekunden ein Event auszulösen, welches die Methode iterateBoard()
aufruft und dadurch die nächste Population im Game of Life erzeugt und das Board neu zeichnet.
Mit timeline.setCycleCount(Timeline.INDEFINITE);
sorgt man dafür, dass dieser Prozess nicht endet. Es wäre auch denkbar eine Abbruchbedingung zu definieren. Beispielsweise, dass die Timeline unterbrochen wird, wenn sich auf dem Board nichts mehr verändert.
Durch den Aufruf board.initBoard(0.5);
sorgt man dafür, dass ein paar lebende Zellen auf dem Board platziert werden. Diesen Wert kann man beliebig zwischen 0 und 1 anpassen.
In den beiden for
-Schleifen wird die GUI des Boards erstellt. Jede Zelle wird durch eine StackPane
repräsentiert. Damit beim Iterieren des Boards schnell auf die Zellen zugegriffen werden kann, werden die zu zeichnenden Zellen in einer HashMap
abgelegt.
Abgeschlossen wird die Methode start
durch den Aufruf timeline.play()
, der die Animation in Gang gesetzt.
In der Methode iterateBoard()
wird durch den Aufruf von nextPopulation()
die nächste Population von Zellen erstellt. Anschließend wird das Styling der StackPane
s angepaßt, so dass lebende Zellen anders gezeichnet werden, als tote Zellen. Dies wird über die Werte alive-cell
und dead-cell
erreicht, die in der folgenden CSS-Datei gol.css
definiert sind.
.root { -fx-fill: white; } .alive-cell { -fx-background-color: red; -fx-border-color: black; -fx-border-width: 0.1; } .dead-cell { -fx-background-color: white; -fx-border-color: black; -fx-border-width: 0.1; }
Die eigentliche Programm-Logik vom Game of Life ist im Folgenden zu sehen:
package org.hameister.gol; import java.util.Random; public class Board { private int[][] board; public Board(int size) { board = new int[size][size]; } public void setField(int x, int y, int value) { board[x][y] = value; } public int getField(int x, int y) { return board[x][y]; } public int getSize() { return board.length; } public void initBoard(int[][] newBoard) { board = newBoard; } public void initBoard(double density) { Random random = new Random(); for(int x=0; x<board.length;x++) { for(int y=0; y<board.length;y++) { if(random.nextDouble()>density) { board[x][y] = 1; } } } } public void nextPopulation() { int[][] newBoard = new int[board.length][board.length]; for(int x=0; x<board.length;x++) { for(int y=0; y<board.length;y++) { newBoard[x][y] = getField(x, y); //Copy value into new board checkBoard(x, y, newBoard); } } board = newBoard; } public void checkBoard(int x, int y, int[][] newBoard) { int[] indexX = {-1,0 ,1 ,-1,1 ,-1,0 ,1 }; int[] indexY = {1 ,1 ,1 ,0 ,0 ,-1,-1,-1}; int fieldValue = board[x][y]; int neighbours = 0; for(int i=0;i<8;i++) { if(x+indexX[i]>=0 && y+indexY[i]>=0 && x+indexX[i]<board.length && y+indexY[i]<board.length) { neighbours = neighbours + getField(x+ indexX[i], y+indexY[i]); } } if(fieldValue==0 && neighbours==3) { //setField(x, y, 1); // Reborn with three alive neighbours newBoard[x][y] = 1; return; } if(fieldValue==1 && neighbours<2) { //setField(x, y, 0); //Less than two alive neightours die newBoard[x][y] = 0; return; } if(fieldValue==1 && (neighbours==2 || neighbours==3)) { // Stay alive if two or three alive neighbours return; } if(fieldValue==1 && neighbours>3) { //setField(x, y, 0); //Die if more than three alive neighbours newBoard[x][y] = 0; return; } } }
Dazu ist eigentlich nichts weiter zu sagen. Das Board ist als zweidimensionales Array realisiert. Eine von unzähligen Möglichkeiten GoL zu implementieren. :-)
Eine Erweiterungsmöglichkeit wäre zusätzlich die vorhergehende Population in einem anderen Farbton darzustellen. Außerdem könnte man das Feld dynamisch gestalten oder durch einem Mouse-Click neue Zellen erstellen...
Weitere Informationen zum Game of Life findet man hier.