我写这篇文章的目的是学习JavaFX,并以此作为重新制作《人生游戏》的借口。这是我编写过的最复杂的GUI,因此主要希望提供反馈,但是我欢迎提出任何批评!

我的版本与原始版本不同,因为单元继承了邻居的颜色出生时会产生有趣的“战争”场景。

从技术上讲,它也是“ 1人”游戏,因为用户可以通过在现场绘制新的活细胞进行干预来影响进化。

示例:




我想评论的主要内容: >
控件影响的所有设置都是全局的;它们是应用程序类的(private)实例成员。我知道这通常是不好的做法,但是我不确定如何设置它。如果我有一个需要更改设置的事件处理程序,而没有通过处理程序和主循环将每个设置线程化,那么我不确定该如何实现。
常规设置代码中的控件。是否有创建/设置控件的标准方法?在这里,我根据控件所属的栏将控件的设置分为功能。它可以工作,但是我看到它失去了控制权,增加了更多控件。
关于GUI布局的任何事情。

请注意,当前这不是“真正的” GOL克隆,因为它不是无限的。当一个单元退出屏幕时,它不再“可见”,并被下一代擦除。如果经典模式与世界末日相撞,则会导致其破坏。

GameOfLife.java:

package gameOfLife;

import static utils.Utils.clearCanvas;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.control.TextField;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.CycleMethod;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.TextAlignment;
import javafx.stage.Stage;
import utils.MainLoop;
import utils.Position;

public class GameOfLife extends Application {

    private final int startCanvasWidth = 1500, startCanvasHeight = 800;
    private double gameSpeedSecs = 0.016667;

    //Global settings to be modified by the controls
    private boolean isPaused = false;
    private boolean clearScreen = true;
    private int birthRadiusOnClick = 5;
    private Color drawColor = Color.BLACK;
    private int cellsWide = 300;
    private int cellsHigh = 300;

    //Settings that can be looked up directly when needed
    private final Slider lifeChanceSlider = new Slider(0, 1, 0.5);
    private final Slider cellsWideSlider = new Slider(10, 800, 200);
    private final Slider cellsHighSlider = new Slider(10, 800, 200);
    private final Label statusLabel = new Label("");

    private Stage mainStage;
    private Scene mainScene;

    private Canvas canvas = new Canvas(startCanvasWidth, startCanvasHeight);
    private GraphicsContext gc = canvas.getGraphicsContext2D();

    private final Random randGen = new Random(993061001);

    private final List<Color> colors = Arrays.asList(Color.BLUE, Color.DARKGOLDENROD, Color.RED, Color.GREEN,
            Color.DARKCYAN, Color.PURPLE, Color.BLACK, Color.CHOCOLATE, Color.AQUA,
            Color.FUCHSIA, Color.HOTPINK, Color.TURQUOISE, Color.AQUAMARINE, Color.CRIMSON,
            Color.BLANCHEDALMOND, Color.THISTLE, Color.DARKORCHID);

    private final Environment<Color> env = new Environment<Color>();

    public static void main(String[] args) {
        launch(args);
    }

    private double getCanvasWidth() {
        return canvas.getWidth();
    }

    private double getCanvasHeight() {
        return canvas.getHeight();
    }

    /**
     * Gets the average (between height and width) ratio between the chosen screen size and the area of the environment
     * @return The average ratio
     */
    private double getAverageScalingFactor() {
        return (getCanvasWidth() + getCanvasHeight()) / (cellsWide + cellsHigh);
    }

    /**
     * Gets the chosen dimensions of the environment as decided by the sliders.
     * @return a Duple representing the chosen (width, height) of the area
     */
    private Position<Integer> getCellAreaBySliders() {
        return new Position<Integer>(
            (int)cellsWideSlider.getValue(),
            (int)cellsHighSlider.getValue() );
    }

    /**
     * Gets the chance that a cell should live during a repopulation as decided by the slider.
     * @return The value of the slider
     */
    private double getLifeChanceBySlider() {
        double lifeChance = lifeChanceSlider.getValue();

        assert (lifeChance >= 0 && lifeChance <= 1) : ("Life Chance out of Range: " + lifeChance);

        return lifeChance;
    }

    /**
     * Finds the chosen area as decided by the sliders, and sets it.
     */
    private void setCellAreaBySliders() {
        Position<Integer> cellArea = getCellAreaBySliders();

        cellsWide = cellArea.getX();
        cellsHigh = cellArea.getY();
    }

    /**
     * Resets the population according to the controls
     */
    private void rePopulateAccordingToControls() {
        Position<Integer> popArea = getCellAreaBySliders();

        env.randomizePopulation(0, popArea.getX(), 0, popArea.getY(), getLifeChanceBySlider(), colors, randGen);
    }

    @Override
    public void start(Stage stage) {
        mainStage = stage;
        mainStage.setMaximized(true);

        mainStage.centerOnScreen();

        BorderPane rootNode = new BorderPane();

        mainScene = new Scene(rootNode, mainStage.getWidth(), mainStage.getHeight());

        //To "draw" cells
        canvas.setOnMousePressed(mouseHandler);
        canvas.setOnMouseDragged(mouseHandler);

        mainStage.setScene(mainScene);

        HBox toolBar = new HBox(5);
        setupTopBar(toolBar);

        VBox rePopBar = new VBox(5);
        setupRePopBar(rePopBar);
        setupRePopSliders();

        HBox colorBar = new HBox(5);
        setupColorBar(colorBar, 50);

        rootNode.setTop(toolBar);
        rootNode.setCenter(canvas);
        rootNode.setRight(rePopBar);
        rootNode.setBottom(colorBar);

        env.randomizePopulation(0, cellsWide, 0, cellsHigh, 0.6, colors, randGen);

        MainLoop mainLoop = new MainLoop((long)(gameSpeedSecs * 1000000000L),
            elapsedNS -> {

                Color backgroundColor = Color.WHITE;

                if (clearScreen) clearCanvas(gc);

                if (isPaused) {
                    gc.setTextAlign(TextAlignment.CENTER);
                    gc.setFont(Font.font(70));
                    gc.fillText("PAUSED", mainStage.getWidth() / 2,
                                            mainStage.getHeight() / 2);

                } else {
                    //To decide how big each cell should be
                    double scalingFactor = getAverageScalingFactor();

                    gc.setFill(backgroundColor);
                    gc.fillRect(0, 0, canvas.getWidth(), canvas.getHeight());

                    //Loop over the environment, drawing a cell if it's alive
                    Environment.forAllCells(0, cellsWide, 0, cellsHigh, (x, y) -> {
                        Color cellColor = env.getCellSpecies(x, y);
                        if (cellColor != null) {

                            double sX = x * scalingFactor, sY = y * scalingFactor,
                                    sSize = scalingFactor;

                            gc.setFill(cellColor);
                            gc.fillRect(sX, sY, sSize, sSize);
                        }


                    });     

                env.simGeneration(0, cellsWide, 0, cellsHigh);

            }
        });

        mainLoop.start();

        mainStage.show();
    }

    public void setupTopBar(HBox toolBar) {
        Button pauseButton = new Button("Pause");
        pauseButton.setOnMouseClicked(pauseHandler);
        toolBar.getChildren().add(pauseButton);

        Button clearButton = new Button("Clear");
        clearButton.setOnMouseClicked(clearHandler);
        toolBar.getChildren().add(clearButton);

        Button toggleClearButton = new Button("Toggle ScreenClear");
        toggleClearButton.setOnMouseClicked(toggleClearHandler);
        toolBar.getChildren().add(toggleClearButton);

        Label dropLabel = new Label("Drop n*n square:");
        toolBar.getChildren().add(dropLabel);

        TextField birthRadText = new TextField();
        birthRadText.setOnAction(birthRadHandler);
        toolBar.getChildren().add(birthRadText);
    }

    private void updateStatusLabel() {
        Position<Integer> area = getCellAreaBySliders();

        statusLabel.setText(
            area.getX() + " x " + area.getY() + "\n" + (int)(getLifeChanceBySlider() * 100) + "% chance"
        );
    }

    private void setupRePopBar(VBox rePopBar) {
        rePopBar.getChildren().add(new Label("Cell Life Chance: "));
        rePopBar.getChildren().add(lifeChanceSlider);

        rePopBar.getChildren().add(new Label("Cells Wide: "));
        rePopBar.getChildren().add(cellsWideSlider);

        rePopBar.getChildren().add(new Label("Cells High: "));
        rePopBar.getChildren().add(cellsHighSlider);

        Button submitRePopButton = new Button("Repopulate");
        rePopBar.getChildren().add(submitRePopButton);
        submitRePopButton.setOnMouseClicked( e -> {
            setCellAreaBySliders();
            rePopulateAccordingToControls();
        });

        rePopBar.getChildren().add(statusLabel);

        rePopBar.setPrefWidth(400);
    }

    /**
     * Creates a colored box that sets the main drawing color to its color when clicked
     * 
     * @param color The color to paint the box and to set the drawing color to when pressed
     * @param sideLength The side length to make the square box
     * @return The created box
     */
    public Rectangle newColorBox(Color color, double sideLength) {
        Rectangle box = new Rectangle(sideLength, sideLength);

        box.setFill(color);

        box.setOnMouseClicked( event -> {
            drawColor = color;
        });

        return box;
    }

    /**
     * Generates a list of stops between each color to create a gradient over each color
     * 
     * @param colors The list of colors to create a gradient for
     * @return A list of stops for the given colors
     */
    private static List<Stop> evenStops(List<Color> colors) {
        List<Stop> stops = new ArrayList<Stop>(colors.size());

        for (int i = 0; i < colors.size(); i++) {
            Stop s = new Stop((double)i / colors.size(), colors.get(i));

            stops.add(s);
        }

        return stops;
    }

    private void setupColorBar(HBox colorBar, double boxSideLength) {
        for (Color c : colors) {
            colorBar.getChildren().add(newColorBox(c, boxSideLength));
        }

        Rectangle randBox = new Rectangle(boxSideLength, boxSideLength);

        randBox.setFill(new LinearGradient(0, 0, 1, 1, true, CycleMethod.REFLECT, evenStops(colors)));
        randBox.setOnMouseClicked( e -> { drawColor = colors.get(randGen.nextInt(colors.size())); });

        colorBar.getChildren().add( randBox );

    }

    private void setupLabelSliderHandlers(Slider slider) {
        slider.setOnScroll( e -> updateStatusLabel()); 
        slider.setOnKeyReleased( e -> updateStatusLabel()); 
        updateStatusLabel();
    }

    private void setupRePopSliders() {
        lifeChanceSlider.setShowTickMarks(true);
        lifeChanceSlider.setMajorTickUnit(0.2);
        lifeChanceSlider.setMinorTickCount(1);
        lifeChanceSlider.setSnapToTicks(true);
        lifeChanceSlider.setBlockIncrement(0.1);
        setupLabelSliderHandlers(lifeChanceSlider);

        cellsWideSlider.setShowTickMarks(true);
        cellsWideSlider.setMajorTickUnit(50);
        cellsWideSlider.setMinorTickCount(1);
        cellsWideSlider.setSnapToTicks(true);
        cellsWideSlider.setBlockIncrement(10);
        setupLabelSliderHandlers(cellsWideSlider);

        cellsHighSlider.setShowTickMarks(true);
        cellsHighSlider.setMajorTickUnit(50);
        cellsHighSlider.setMinorTickCount(1);
        cellsHighSlider.setSnapToTicks(true);
        cellsHighSlider.setBlockIncrement(10);
        setupLabelSliderHandlers(cellsHighSlider);
    }

    //Event Handlers

    EventHandler<Event> pauseHandler = event -> {
        isPaused = !isPaused;
    };

    EventHandler<MouseEvent> mouseHandler = event -> {
        double scale = getAverageScalingFactor();
        int sX = (int)(event.getX() / scale),
            sY = (int)(event.getY() / scale);

        Environment.forAllCells(sX, sX, sY, sY, birthRadiusOnClick, (x,y) -> {
            env.setCell(x, y, drawColor);
        });
    };

    EventHandler<Event> clearHandler = event -> {
        clearCanvas(gc);
    };

    EventHandler<Event> toggleClearHandler = event -> {
        clearScreen = !clearScreen;
    };

    /**
     * Attempts to parse the given text as a integer and set the draw radius.<p>
     * 
     * Outputs an error to the System.out in the event of a parsing error.
     */
    EventHandler<ActionEvent> birthRadHandler = event -> {
        TextField textField = (TextField)event.getSource();

        String enteredText = textField.getCharacters().toString();

        try {
            birthRadiusOnClick = Integer.parseInt(enteredText);

        } catch(NumberFormatException e) {
            System.out.println("Invalid birth radius entered: " + enteredText);
        }
    };

}


Environment.java :

package gameOfLife;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.function.BiConsumer;

import utils.Position;

/**
 * An object representing a Game of Life environment.
 * 
 * @author Brendon
 *
 * @param <S> The species representation of the cells
 */
public class Environment<S> {

    /**
     * The currently living cells. Any cell not explicitly alive is considered dead.
     */
    private Map<Position<Integer>, S> liveCells = new HashMap<Position<Integer>, S>();

    //Cached as instance member to avoid repeated constant construction/destruction
    private final SpeciesFreqs speciesFreqs = new SpeciesFreqs();

    /**
     * Finds what species a cell at a certain cell is, or null if the cell is dead
     * 
     * @param x The x-coordinate
     * @param y The y-coordinate
     * @return The species of the cell at (x,y) if alive, or null if dead
     */
    public S getCellSpecies(int x, int y) {
        return liveCells.get(new Position<Integer>(x, y));
    }

    /**
     * Generates a {@link SpeciesFreqs} of the cells surrounding (x,y).<p>
     * A range of 1 scans the 8 immediate cells surrounding (x,y) (default behavior)
     * 
     * @param x The x-coordinate
     * @param y The y-coordinate
     * @param range The depth to check from the centre coordinate.
     * @return A reference to the instance speciesFreqs recorder.
     */
    private SpeciesFreqs getNeighborsOf(int x, int y, int range) {
        speciesFreqs.reset();

        for (int checkY = y - range; checkY <= y + range; checkY++) {
            for (int checkX = x - range; checkX <= x + range; checkX++) {
                Position<Integer> pos = new Position<Integer>(checkX, checkY);
                S cellSpecies = liveCells.get(pos);

                if (cellSpecies != null && !(checkX == x && checkY == y)) {

                    speciesFreqs.add(cellSpecies);
                }
            }
        }

        return speciesFreqs;
    }

    /**
     * Figures out whether or not the cell at (x,y) is alive or dead, and what
     *  species it should adopt.
     *  
     * @param x The x-coordinate to check
     * @param y The y-coordinate to check
     * @return The species the cell should become, or null if it's dead.
     */
    private S findStateOfCell(int x, int y) {
        SpeciesFreqs neighborSpecies = getNeighborsOf(x, y, 1);
        int neighbors = neighborSpecies.getNNeighbors();

        S domSpecies = neighborSpecies.getDomSpecies();
        boolean isCurrentlyAlive = getCellSpecies(x,y) != null;

        if (isCurrentlyAlive) {
            if (neighbors == 2 || neighbors == 3)
                return domSpecies;
            else return null;

        } else {
            if (neighbors == 3)
                return domSpecies;
            else return null;
        }

    }

    /**
     * A compacted 2D for-loop
     * @param xMin The starting x-value for the loop
     * @param xMax The inclusive max x-value
     * @param yMin The starting y-value for the loop
     * @param yMax The inclusive max y-value
     * @param f The body of the loop
     */
    public static void forAllCells(int xMin, int xMax, int yMin, int yMax, BiConsumer<Integer, Integer> f) {
        for (int checkY = yMin; checkY <= yMax; checkY++) {
            for (int checkX = xMin; checkX <= xMax; checkX++) {
                f.accept(checkX, checkY);
            }
        }
    }

    /**
     * A compacted 2D for-loop<p>
     * An convenience version that automatically subtracts the range from the minimum values, and adds it to the maximum values.<p>
     * 
     * In the x-dimension the loop starts at xMin - range, and extends to (inclusive) xMax + range
     * 
     * @param xMin The starting x-value for the loop
     * @param xMax The inclusive max x-value
     * @param yMin The starting y-value for the loop
     * @param yMax The inclusive max y-value
     * @param range The amount to increase the loop by in each "direction"
     * @param f The body of the loop
     */
    public static void forAllCells(int xMin, int xMax, int yMin, int yMax, int range, BiConsumer<Integer, Integer> f) {
        forAllCells(xMin - range, xMax + range, yMin - range, yMin + range, f);
    }

    /**
     * Allows a cell to be forcibly made alive. <p>
     * Replaces old cells at the target location
     * 
     * @param x The x-coordinate of the cell to set
     * @param y The y-coordinate of the cell to set
     * @param species The species of the cell to make it alive, or null to kill it
     */
    public void setCell(int x, int y, S species) {
        liveCells.put(new Position<Integer>(x,y), species);
    }

    /**
     * Advances the environment by 1 "generation".<p>
     * After calling, the population will have been replaced by the new generation as determined by the rules.<p>
     * Only "scans" and updates the area indicated by the ranges
     * 
     * 
     * @param xMin The minimum value in the x-dimension to affect
     * @param xMax The maximum value (inclusive) in the x-dimension to affect
     * @param yMin The minimum value in the y-direction to affect
     * @param yMax The maximum value (inclusive) in the y-direction to affect
     */
    public void simGeneration(int xMin, int xMax, int yMin, int yMax) {
        Map<Position<Integer>, S> newLiveCells = new HashMap<Position<Integer>, S>();

        forAllCells(xMin, xMax, yMin, yMax, (x, y) -> {
            S cellState = findStateOfCell(x, y);

            if (cellState != null) {
                newLiveCells.put(new Position<Integer>(x, y), cellState);
            }

        });

        liveCells = newLiveCells;
    }

    /**
     * Clears and replaces the current population with a randomized population with the indicated traits within the indicated bounds.
     * 
     * @param xMin The minimum value in the x-dimension to populate
     * @param xMax The maximum value (inclusive) in the x-dimension to populate
     * @param yMin The minimum value in the y-direction to populate
     * @param yMax The maximum value (inclusive) in the y-direction to populate
     * @param chanceOfLife The decimal chance that any cell with become alive. (<= 0) results in an empty population, (>= 1) results in a full population
     * @param possibleSpecies The range of possible species 
     * @param randGen A random generator
     */
    public void randomizePopulation(int xMin, int xMax, int yMin, int yMax,
            double chanceOfLife, List<S> possibleSpecies, Random randGen) {

        liveCells.clear();

        forAllCells(xMin, xMax, yMin, yMax, (x, y) -> {
            if (randGen.nextDouble() <= chanceOfLife) {
                liveCells.put(new Position<Integer>(x, y), possibleSpecies
                        .get(randGen.nextInt(possibleSpecies.size())));
            }
        });
    }

    @Override
    public String toString() {
        StringBuilder strB = new StringBuilder();

        for (int checkY = 0; checkY <= 10; checkY++) {
            for (int checkX = 0; checkX <= 10; checkX++) {
                S cellSpecies = getCellSpecies(checkX, checkY);
                strB.append((cellSpecies != null ? cellSpecies : ' ') + " ");
            }

            strB.append('\n');
        }

        return strB.toString();
    }

    public static void main(String[] args) {
        Random randGen = new Random(993061003);

        int xB = 9, yB = 9;

        Environment<Character> env = new Environment<>();
        // env.randomizePopulation(0, xB, 0, yB, 0.1, randGen);

        env.liveCells.put(new Position<Integer>(2, 1), 'A');
        env.liveCells.put(new Position<Integer>(2, 2), 'B');
        env.liveCells.put(new Position<Integer>(2, 3), 'C');

        for (int i = 0; i < 3; i++) {
            System.out.println("____________________\n" + env);

            env.simGeneration(0, xB, 0, yB);
        }
    }

    /**
     * A simple mutable integer class to assist in efficient species counting.
     * 
     * @author Brendon
     *
     */
    static class MutableInt {
        private int val;

        /**
         * Defaults to a value of 0
         */
        public MutableInt() {
            this(0);
        }

        public MutableInt(int v) {
            val = v;
        }

        public void inc() {
            val++;
        }

        public int getVal() {
            return val;
        }

        @Override
        public String toString() {
            return String.valueOf(val);
        }
    }

    /**
     * A class to assist in counting the number of each species surrounding a cell.
     * @author Brendon
     *
     */
    class SpeciesFreqs {
        private final Map<S, MutableInt> speciesFreqs = new HashMap<>();
        private int totalNeighbors = 0;

        /**
         * Adds an instance of a species to the count
         * @param species
         */
        public void add(S species) {
            MutableInt count = speciesFreqs.get(species);

            if (count == null) {
                speciesFreqs.put(species, new MutableInt(1));

            } else {
                count.inc();
            }

            totalNeighbors++;
        }

        /**
         * Clears the recorded neighbors 
         */
        public void reset() {
            speciesFreqs.clear();
            totalNeighbors = 0;
        }

        /**
         * Returns the total number of recorded neighbors
         * @return The total number of neighbors recorded since the last reset 
         */
        public int getNNeighbors() {
            return totalNeighbors;
        }

        /**
         * Figures out the dominant counted color. Considers the last species added to be dominant in the event of a tie. 
         * @return The most-occurring species counted, or the last species in the event of a tie.
         */
        public S getDomSpecies() {
            if (speciesFreqs.isEmpty()) return null;

            S domSpecies = null;
            int highFreq = 0;

            for (Map.Entry<S, MutableInt> entry : speciesFreqs.entrySet()) {
                S species = entry.getKey();
                int freq = entry.getValue().getVal();

                if (freq >= highFreq) {
                    highFreq = freq;
                    domSpecies = species;
                }
            }

            return domSpecies;
        }

    }
}


MainLoop.java:

package utils;

import java.util.function.Consumer;

import javafx.animation.AnimationTimer;

/**
 * An extension of an {@link AnimationTimer} that allows the user to select their framerate.<p>
 * 
 * Also allows for a more convenient syntax to declare the main loop routine
 * 
 * @author Brendon
 *
 */
public class MainLoop extends AnimationTimer {

    private final long updateGraphicsEvery;

    private final Consumer<Long> doEveryUpdate;

    private long lastTime = IDEALFRAMERATENS;

    /**
     * @param updateEveryNS How often to run the loop.
     * @param f The main-loop body. Its parameter is the number of nanoseconds since the last update.
     */
    public MainLoop(long updateEveryNS, Consumer<Long> f) {
        this.updateGraphicsEvery = updateEveryNS;
        doEveryUpdate = f;
    }

    @Override
    public void handle(long currentTime) {

        long nanosElapsed = currentTime - lastTime;

        if (nanosElapsed < updateGraphicsEvery) {
            return;

        } else {
            lastTime = currentTime;
            doEveryUpdate.accept(nanosElapsed);

        }

    }

    public final static long NANOSPERSECOND = 1000000000;
    public final static long IDEALFRAMERATENS = (long)(1 / 60.0 * NANOSPERSECOND);

}


Utils.java:

package utils;

import java.util.function.Consumer;

import javafx.animation.AnimationTimer;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;

public class Utils {
    private Utils() {

    }

    /**
     * Clears the canvas associated with the given {@link GraphicsContext}
     * 
     * @param gc The associated GraphicsConetext to clear
     */
    public static void clearCanvas(GraphicsContext gc) {
        Canvas c = gc.getCanvas();
        gc.clearRect(0, 0, c.getWidth(), c.getHeight());
    }

}


Position.java:

package utils;

import java.util.function.BinaryOperator;
import java.util.function.UnaryOperator;

/**
 * A simple immutable Duple class where both members are the same type
 * 
 * @author Brendon
 *
 * @param <T> The type of its members
 */
public class Position<T> {

    private final T xPos;
    private final T yPos;

    public Position(T x, T y) {
        xPos = x;
        yPos = y;
    }

    public T getX() {
        return xPos;
    }

    public T getY() {
        return yPos;
    }

    /**
     * Applies the function to each member of the Duple
     * @param f The function to apply to each member
     * @return The resulting Position
     */
    public Position<T> map(UnaryOperator<T> f) {
        return new Position<T>(f.apply(xPos), f.apply(yPos)); 
    }

    /**
     * Applies the function to this and the other Position
     * @param otherPos The other Position to use
     * @param f A function taking 2 Positions, where the first parameter is a member of this, and the second is a member of otherPos
     * @return The resulting Position
     */
    public Position<T> map(Position<T> otherPos, BinaryOperator<T> f) {
        return new Position<T>(f.apply(xPos, otherPos.xPos), f.apply(yPos, otherPos.yPos)); 
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((xPos == null) ? 0 : xPos.hashCode());
        result = prime * result + ((yPos == null) ? 0 : yPos.hashCode());
        return result;
    }


    @SuppressWarnings("rawtypes")
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;
        Position other = (Position) obj;
        if (xPos == null) {
            if (other.xPos != null) return false;
        } else if (!xPos.equals(other.xPos)) return false;
        if (yPos == null) {
            if (other.yPos != null) return false;
        } else if (!yPos.equals(other.yPos)) return false;
        return true;
    }

    @Override
    public String toString() {
        return "(" + xPos + "," + yPos + ")";
    }

}


评论

只是附带说明,“切换屏幕清除”和“清除”按钮可在各代之间进行屏幕清除。它们与生活游戏无关,但会产生有趣的效果,看起来就像一幅画本身。它们可能会在将来的版本中删除,因此可以忽略。

@Antot添加了。请注意2件事:1.自此我将Position重命名为Duple(尽管未在此代码中反映出来),2. MainLoop,Utils和Position是utils包的一部分,因此请确保对其进行修改或设置相应地打包。

双重?您是要命名为Pair吗? Duple是什么意思?

@Justin不久前,我听说Duple用作2元素元组的特殊情况,但是我很难找到类似的用法。您说得对,应该是Pair。

在我开始审查它之前(例如,使用新的类名),您能用最新的代码更新它吗?否则,我会给出一些建议,要么不适用,要么您已经提出建议。

#1 楼

命名

清单常量通常在Java中使用_来表示空间。因此,例如NANOSPERSECOND应该是NANOS_PER_SECOND。 br避免不必要的转换

这里不需要双精度:

public final static long IDEALFRAMERATENS = (long)(1 / 60.0 * NANOSPERSECOND);


只需交换操作:

public final static long IDEALFRAMERATENS = NANOSPERSECOND / 60;


结果是一样的,因为无论如何您都被截断了。

模型视图控制器(非)分离MainLoop类将模型,视图和控制器的所有三个混合在一起。非常希望将它们分开。我要做的是将与游戏模拟有关的所有事情分离:GameOfLifeisPausedcellsWide/High等,并放入新的类mainloop中。接下来,我将UI设计与类分开,并将其放入描述UI的FXML文档中。然后,对于FXML文档,我将有一个单独的控制器类,而将应用程序类作为一个非常基本的启动器保留。

使用FXML将使您可以将事件处理程序创建为控制器中的成员函数。因此,您将拥有:

@FXML
public void startStopSimulation(){
    isPaused = !isPaused;
}


,在fxml中,您将拥有一个名为GameOfLifeSimulator的属性。将按钮动作链接到控制器中的方法。

这里将帮助您开始使用FXML。

使用onAction="startStopSimulation"


JavaFX控件基于可观对象,属性和绑定的强大概念。可观察对象是包装对象的类,该类使您可以侦听对象中的更改。属性是可观察的值。绑定是一种使用属性值可观察的事实创建绑定到属性值的表达式的方法。因此,如果属性更改,则依赖于该属性的所有绑定也会更改。最后,您可以将属性的值绑定到绑定,以便可以有效地将属性彼此链接,并创建属性的任意表达式,然后将这些表达式绑定到其他属性。例如,您可以执行以下操作:

NumberBinding averageScaleFactor = Bindings.divide(
    canvas.widthProperty().add(canvas.heightProperty()),
    cellsWideSlider.valueProperty().add(cellsHighSlider.valueProperty()));


要创建绑定,一种数值表达式可以

,只要需要比例因子,只需调用:properties。画布或滑块更改后,结果将被缓存并自动更新。因此,以上代码片段将消除averageScaleFactor.get()方法。

使用Model-View-Controller的想法,即“模型”,getAverageScalingFactor将具有:GameOfLifeSimulator,您可以将其绑定到您的“控制器”中的用户界面(“视图”)。并且它们将自动保持同步。

避免在游戏循环中分配新对象。

我看到您经常在游戏代码中分配新的IntegerProperty cellsWide/High;元素。尽管在Java中内存分配很便宜,但是像您所做的那样频繁分配对象并将其丢弃会给GC造成很大的压力,并可能导致游戏看起来“抖动”。我建议一次分配位置对象,并在有意义的地方重新使用它们。

例如,在这里:

private SpeciesFreqs getNeighborsOf(int x, int y, int range) {
    speciesFreqs.reset();

    for (int checkY = y - range; checkY <= y + range; checkY++) {
        for (int checkX = x - range; checkX <= x + range; checkX++) {
            Position<Integer> pos = new Position<Integer>(checkX, checkY);
            S cellSpecies = liveCells.get(pos);

            if (cellSpecies != null && !(checkX == x && checkY == y)) {

                speciesFreqs.add(cellSpecies);
            }
        }
    }


您为每个$$ 4 \ cdot范围^ 2 $$元素分配一个新位置您只需在顶部分配一次Position并重新使用它就可以摆脱困境。

或者在这里:

public S getCellSpecies(int x, int y) {
    return liveCells.get(new Position<Integer>(x, y));
} 


或者在这里:

public void setCell(int x, int y, S species) {
    liveCells.put(new Position<Integer>(x,y), species);
}


pos对象的整个分配和处置似乎以您正在使用Position代表游戏的真实状态为中心。这也可能是性能下降的原因,对地图的每次访问都要求您必须一遍又一遍地计算这些位置的哈希码。

如果仅使用Map<Position,S>作为键并使用像Integer这样的自定义哈希函数,则可能会更快。该函数没有冲突,并且比x + MAX_WIDTH*y类的hashCode计算速度更快。另外,还可以避免使GC疲劳。

另一件事可能甚至更快,就是简单地拥有一个大型数组Position,让死细胞成为List<S> cells = new ArrayList<>(cellsWide * cellsHigh);。访问由null完成,它避免了查找映射和哈希码的计算。它还将允许您放弃在整个位置分配cells.get(x + cellsWide*y)对象。但是,这确实意味着游戏的调整大小行为会发生变化,并且在发生这种情况时您将不得不重新分配并复制该字段。但是,与运行仿真的频率相比,这是很少见的事件,因此我相信您会在此方面获得性能。

摘要

在这篇评论中很有用。最好的办法是分离模型,视图和控制器,并减少游戏代码中GC的负担。 Position对象的频繁分配和处置以及昂贵的hashCode函数可能会导致性能问题。

评论


\ $ \ begingroup \ $
太好了,谢谢。我还不会接受这一点,因为其他人已经说过他们也计划进行审查。尽管他们似乎已经忘记了。
\ $ \ endgroup \ $
–致癌物质
16年1月8日在15:26

\ $ \ begingroup \ $
@Carcigenicate不是那个女孩,但这是一个月前:)
\ $ \ endgroup \ $
–艾米莉·L。
16年1月8日在22:08

\ $ \ begingroup \ $
我知道:/。我希望他们在某个时候记得。不用担心,我偶尔会翻阅我的帖子,并接受所有未解决的问题。
\ $ \ endgroup \ $
–致癌物质
16年1月8日在22:10

\ $ \ begingroup \ $
对不起,我忘了。正如承诺的那样,我回来了!再次感谢。
\ $ \ endgroup \ $
–致癌物质
16 Feb 12'at 0:33