JavaFX - Game of Life

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.StackPanes 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.)

GameOfLife

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 StackPanes 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.