0

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):

  • Using shapes to create a clip mask for a pane (Can only use one mask per node)
  • Having a complete deep copy of each node for each view (too expensive, nodes moving constantly)
  • Having x number of users each have their own set of nodes and have one update loop update every node in every view (too expensive, too many nodes for scene graph, too much)

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.

NotZack
  • 518
  • 2
  • 9
  • 22
  • This question is quite broad, but I think this may be a case of choosing the wrong tool for the job. JavaFX may not be the best choice of framework for such a complex graphical application. – Zephyr Apr 18 '19 at 03:14
  • @Zephyr Thanks for the reply. That's what I'm leaning towards, but I didn't want want to discount JavaFx just because I dont know all the ins and outs. Thanks for the insight! – NotZack Apr 18 '19 at 03:19
  • Not sure what exactly you're displaying, but binding certain properties of the nodes may be an option... "Update" loops, especially multiple of them are seldomly a good idea. If you do need such a loop, try grouping the updates in a single "loop". – fabian Apr 18 '19 at 08:58
  • Let each view listen to a common model; each view can then update itself, limiting changes to nodes that fall within its purview. – trashgod Apr 18 '19 at 09:48
  • Thank you both, I will look further into binding properties to a common model – NotZack Apr 18 '19 at 12:45
  • If you loop your model in a background `Task`, beware of the pitfall mentioned [here](https://stackoverflow.com/questions/55727835/need-clarification-on-changing-data-in-javafx-application-thread#comment98141184_55730021). – trashgod Apr 18 '19 at 17:22
  • @trashgod Wow, I was about the fall for that! I'll get back in a few days with an answer on how I did things. Thank you very much again – NotZack Apr 18 '19 at 17:26
  • Excellent; for reference, this [example](https://stackoverflow.com/a/44056730/230513) has a `Task` that invokes `updateValue(canvas)`; yours might have a `Task`. – trashgod Apr 18 '19 at 18:13

1 Answers1

0

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

NotZack
  • 518
  • 2
  • 9
  • 22