javajavafxviewportsplit-screen

Multiple viewports in javafx application


I am attempting to create a multi-user, multi-screen application within JavaFX, and I am having trouble with the multi-screen part.

Think an FPS with couch co-op: the screen splits evenly depending on how many people are connected locally. Every different view is looking in a different direction, and at a different place, but at the same 'world'.

I learned the hard way (confirmed in a comment here) that each node can only appear in the active scene graph once, so, for instance, I cannot have the same node spread across multiple distinct panes (which is conceptually ideal). And that's where I'm not sure where to go next.

Looking at other similar technologies like OpenGL, (example) most have the ability to create another viewport for the application, but JavaFX does not seem to have this.

Some things I have ruled out as unreasonable/impossible (correct me if I'm wrong):

How would I go about creating multiple views of the same set of nodes, while still maintaining individual user control, and changing persistence/moving nodes, between every different view?

Thanks.


Solution

  • Thanks to the people in the comments for the solution. I ended up creating a background model for each view to mirror, and then creating a new set of nodes per view that has the relevant properties bound to the background model.

    The update loop then has to only update the one background model, and the copies all update automatically. Each node copy has a reference to the model node that it is mimicking, so when a user inputs a change for a node, the model node is changed, which changes the copy node.

    The solution is not too elegant and I will have to look more into multithreading (multitasking?) with Tasks (here) and Platform.runLater() (here) functions of JavaFX to increase functionality.

    Here is a quick example of what I accomplished:

    Proof Of Concept Gif

    Main.java

    public class Main extends Application {
    
        private static Group root = new Group();
        private static Scene initialScene = new Scene(root, Color.BLACK);
    
        private static final int NUM_OF_CLIENTS = 8;
    
        private static long updateSpeed = 20_666_666L;
        private static double deltaTime;
        private static double counter = 0;
    
        @Override
        public void start(Stage primaryStage) {
            primaryStage.setFullScreen(true);
            primaryStage.setScene(initialScene);
            primaryStage.show();
    
            initModel();
            initModelViews();
            startUpdates();
        }
    
        private void initModel() {
            for (int i = 0; i < NUM_OF_CLIENTS; i++) {
                Model.add(new UpdateObject());
            }
        }
    
        private void initModelViews() {
            //Correctly positioning the views
            int xPanes = (NUM_OF_CLIENTS / 4.0 > 1.0) ? 4 : NUM_OF_CLIENTS;
            int yPanes = (NUM_OF_CLIENTS / 4) + ((NUM_OF_CLIENTS % 4 > 0) ? 1 : 0);
    
            for (int i = 0; i < NUM_OF_CLIENTS; i++) {
                Pane clientView = new Pane(copyModelNodes());
                clientView.setBackground(new Background(new BackgroundFill(Color.color(Math.random(), Math.random(), Math.random()), CornerRadii.EMPTY, Insets.EMPTY)));
                System.out.println(clientView.getChildren());
                clientView.relocate((i % 4) * (Main.initialScene.getWidth() / xPanes), (i / 4) * (Main.initialScene.getHeight() / yPanes)) ;
                clientView.setPrefSize((Main.initialScene.getWidth() / xPanes), (Main.initialScene.getHeight() / yPanes));
                root.getChildren().add(clientView);
            }
        }
    
        private Node[] copyModelNodes() {
            ObservableList<UpdateObject> model = Model.getModel();
            Node[] modelCopy = new Node[model.size()];
    
            for (int i = 0; i < model.size(); i++) {
                ImageView testNode = new ImageView();
                testNode.setImage(model.get(i).getImage());
                testNode.layoutXProperty().bind(model.get(i).layoutXProperty());
                testNode.layoutYProperty().bind(model.get(i).layoutYProperty());
                testNode.rotateProperty().bind(model.get(i).rotateProperty());
                modelCopy[i] = testNode;
            }
            return modelCopy;
        }
    
        private void startUpdates() {
            AnimationTimer mainLoop = new AnimationTimer() {
                private long lastUpdate = 0;
    
                @Override
                public void handle(long frameTime) {
    
                    //Time difference from last frame
                    deltaTime = 0.00000001 * (frameTime - lastUpdate);
    
                    if (deltaTime <= 0.1 || deltaTime >= 1.0)
                        deltaTime = 0.00000001 * updateSpeed;
    
                    if (frameTime - lastUpdate >= updateSpeed) {
                        update();
                        lastUpdate = frameTime;
                    }
                }
            };
            mainLoop.start();
        }
    
        private void update() {
            counter += 0.1;
    
            if (counter > 10.0) {
                counter = 0;
            }
            for (UpdateObject objectToUpdate : Model.getModel()) {
                objectToUpdate.setLayoutX(objectToUpdate.getLayoutX() + 0.02 * counter * deltaTime);
                objectToUpdate.setLayoutY(objectToUpdate.getLayoutY() + 0.02 * counter * deltaTime);
                objectToUpdate.setRotate(objectToUpdate.getRotate() + 5);
            }
        }
    }
    

    UpdateObject.java

    class UpdateObject extends ImageView {
    
        private static Random random = new Random();
        private static Image testImage = new Image("duckTest.png");
    
        UpdateObject() {
            this.setImage(testImage);
            this.setLayoutX(random.nextInt(50));
            this.setLayoutY(random.nextInt(50));
            this.setRotate(random.nextInt(360));
        }
    }
    

    Model.java

    class Model {
    
        private static ObservableList<UpdateObject> modelList = FXCollections.observableArrayList();
    
        static void add(UpdateObject objectToAdd) {
            modelList.add(objectToAdd);
        }
    
        static ObservableList<UpdateObject> getModel() {
            return modelList;
        }
    }
    

    Test image used