diff --git a/racevisionGame/src/main/java/mock/app/Event.java b/racevisionGame/src/main/java/mock/app/Event.java index 4b8a6bc6..a9f55a7f 100644 --- a/racevisionGame/src/main/java/mock/app/Event.java +++ b/racevisionGame/src/main/java/mock/app/Event.java @@ -219,7 +219,6 @@ public class Event { long millisecondsToAdd = racePreStartTime + racePreparatoryTime; - long secondsToAdd = millisecondsToAdd / 1000; DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ"); diff --git a/racevisionGame/src/main/java/mock/model/MockRace.java b/racevisionGame/src/main/java/mock/model/MockRace.java index 41fbe4d8..9ede32d1 100644 --- a/racevisionGame/src/main/java/mock/model/MockRace.java +++ b/racevisionGame/src/main/java/mock/model/MockRace.java @@ -366,10 +366,8 @@ public class MockRace extends RaceState { //Checks if the current boat has finished the race or not. boolean finish = this.isLastLeg(boat.getCurrentLeg()); - if (!finish && totalElapsedMilliseconds >= updatePeriodMilliseconds) { - + if (!finish && totalElapsedMilliseconds >= updatePeriodMilliseconds && !boat.isColliding()) { if(boat.isVelocityDefault()) setBoatSpeed(boat); - //Calculates the distance travelled, in meters, in the current timeslice. double distanceTravelledMeters = boat.calculateMetersTravelled(updatePeriodMilliseconds) * this.scaleFactor; diff --git a/racevisionGame/src/main/java/mock/model/RaceLogic.java b/racevisionGame/src/main/java/mock/model/RaceLogic.java index 3b74d5c8..0110716e 100644 --- a/racevisionGame/src/main/java/mock/model/RaceLogic.java +++ b/racevisionGame/src/main/java/mock/model/RaceLogic.java @@ -12,6 +12,7 @@ import network.Messages.Enums.RaceStatusEnum; import network.Messages.LatestMessages; import shared.model.RunnableWithFramePeriod; +import java.util.ArrayList; import java.util.Observable; import java.util.Observer; @@ -90,7 +91,7 @@ public class RaceLogic implements RunnableWithFramePeriod, Observer { server.parseSnapshot(); - waitForFramePeriod(previousFrameTime, currentTime, 50); + waitForFramePeriod(previousFrameTime, currentTime, 16); previousFrameTime = currentTime; } } @@ -127,7 +128,7 @@ public class RaceLogic implements RunnableWithFramePeriod, Observer { race.setBoatsStatusToRacing(); } - waitForFramePeriod(previousFrameTime, currentTime, 50); + waitForFramePeriod(previousFrameTime, currentTime, 16); previousFrameTime = currentTime; } @@ -139,6 +140,8 @@ public class RaceLogic implements RunnableWithFramePeriod, Observer { */ private void raceLoop() { + ArrayList collisionBoats = new ArrayList<>(); + long previousFrameTime = System.currentTimeMillis(); while (race.getRaceStatusEnum() != RaceStatusEnum.FINISHED && loopBool) { @@ -168,9 +171,17 @@ public class RaceLogic implements RunnableWithFramePeriod, Observer { //If it is still racing, update its position. if (boat.getStatus() == BoatStatusEnum.RACING) { race.updatePosition(boat, framePeriod, race.getRaceClock().getDurationMilli()); - race.getColliderRegistry().rayCast(boat); + + if(race.getColliderRegistry().rayCast(boat)){ + //System.out.println("Collision!"); + //Add boat to list + collisionBoats.add(boat); + } + + //System.out.println(race.getColliderRegistry().rayCast(boat)); } + } } else { @@ -183,14 +194,19 @@ public class RaceLogic implements RunnableWithFramePeriod, Observer { // Change wind direction race.changeWindDirection(); + //Pass collision boats in + server.parseBoatCollisions(collisionBoats); + //Parse the race snapshot. server.parseSnapshot(); + collisionBoats.clear(); + //Update the last frame time. previousFrameTime = currentTime; } - waitForFramePeriod(previousFrameTime, currentTime, 50); + waitForFramePeriod(previousFrameTime, currentTime, 16); previousFrameTime = currentTime; } } diff --git a/racevisionGame/src/main/java/mock/model/RaceServer.java b/racevisionGame/src/main/java/mock/model/RaceServer.java index 93a154b0..7f213a72 100644 --- a/racevisionGame/src/main/java/mock/model/RaceServer.java +++ b/racevisionGame/src/main/java/mock/model/RaceServer.java @@ -3,6 +3,7 @@ package mock.model; import network.AckSequencer; import network.Messages.*; import network.Messages.Enums.BoatLocationDeviceEnum; +import network.Messages.Enums.YachtEventEnum; import network.Messages.Enums.XMLMessageType; import shared.model.Bearing; import shared.model.CompoundMark; @@ -24,6 +25,7 @@ public class RaceServer { private MockRace race; private LatestMessages latestMessages; private static RaceServer server; + private List collisionEvents = new ArrayList<>(); /** @@ -75,9 +77,19 @@ public class RaceServer { //Parse the race status. snapshotMessages.add(parseRaceStatus()); + //Parse collisions + if(collisionEvents.size()>0){ + snapshotMessages.addAll(collisionEvents); + } + latestMessages.setSnapshot(snapshotMessages); updateXMLFiles(); + + //Reset collision list + collisionEvents.clear(); + //System.out.println(collisionEvents.size()); + } /** @@ -314,4 +326,28 @@ public class RaceServer { return message; } + + /** + * Parse the yacht event and return it + * @param boat yacht with event + * @param event event that happened + * @return yacht event + */ + private YachtEvent parseYachtEvent(MockBoat boat, YachtEventEnum event){ + YachtEvent yachtEvent = new YachtEvent( + System.currentTimeMillis(), + this.boatLocationSequenceNumber, + race.getRaceId(), + boat.getSourceID(), + 1337, + event); + return yachtEvent; + } + + public void parseBoatCollisions(ArrayList boats){ + for (MockBoat boat : boats){ + collisionEvents.add(parseYachtEvent(boat, YachtEventEnum.COLLISION)); + } + } + } diff --git a/racevisionGame/src/main/java/mock/model/collider/Collider.java b/racevisionGame/src/main/java/mock/model/collider/Collider.java index 28c01f9f..3bce3bc3 100644 --- a/racevisionGame/src/main/java/mock/model/collider/Collider.java +++ b/racevisionGame/src/main/java/mock/model/collider/Collider.java @@ -24,7 +24,8 @@ public abstract class Collider extends Observable implements Locatable { // Direction of collider from heading Bearing relative = Bearing.fromDegrees(absolute.degrees() - boat.getBearing().degrees()); - if(actualDistance <= distance) { + if(!boat.isColliding() && actualDistance <= distance) { + boat.setColliding(true); Collision collision = new Collision(boat, relative, distance); // Notify object of collision onCollisionEnter(collision); diff --git a/racevisionGame/src/main/java/mock/model/commandFactory/CollisionCommand.java b/racevisionGame/src/main/java/mock/model/commandFactory/CollisionCommand.java index bb86433d..52ec2b34 100644 --- a/racevisionGame/src/main/java/mock/model/commandFactory/CollisionCommand.java +++ b/racevisionGame/src/main/java/mock/model/commandFactory/CollisionCommand.java @@ -29,17 +29,18 @@ public class CollisionCommand extends ObserverCommand { public void execute() { this.azimuth = Azimuth.fromDegrees(boat.getBearing().degrees() - 180d); this.startingPosition = boat.getPosition(); - this.distance = 30; + this.distance = 60; boat.setVelocityDefault(false); } @Override public void update(Observable o, Object arg) { if(GPSCoordinate.calculateDistanceMeters(boat.getPosition(), startingPosition) < distance) { - boat.setPosition(GPSCoordinate.calculateNewPosition(boat.getPosition(), 2, azimuth)); + boat.setPosition(GPSCoordinate.calculateNewPosition(boat.getPosition(), 3, azimuth)); } else { race.deleteObserver(this); boat.setVelocityDefault(true); + boat.setColliding(false); } } } diff --git a/racevisionGame/src/main/java/mock/model/commandFactory/CompositeCommand.java b/racevisionGame/src/main/java/mock/model/commandFactory/CompositeCommand.java index 74c5e95b..a931a85a 100644 --- a/racevisionGame/src/main/java/mock/model/commandFactory/CompositeCommand.java +++ b/racevisionGame/src/main/java/mock/model/commandFactory/CompositeCommand.java @@ -1,7 +1,7 @@ package mock.model.commandFactory; -import java.util.ArrayDeque; import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedDeque; /** * Wraps multiple commands into a composite to execute queued commands during a frame. @@ -10,15 +10,15 @@ public class CompositeCommand implements Command { private Queue commands; public CompositeCommand() { - this.commands = new ArrayDeque<>(); + this.commands = new ConcurrentLinkedDeque<>(); } public void addCommand(Command command) { - commands.add(command); + commands.offer(command); } @Override public void execute() { - while(!commands.isEmpty()) commands.remove().execute(); + while(commands.peek() != null) commands.poll().execute(); } } diff --git a/racevisionGame/src/main/java/mock/xml/RaceXMLCreator.java b/racevisionGame/src/main/java/mock/xml/RaceXMLCreator.java index 92f68518..75024013 100644 --- a/racevisionGame/src/main/java/mock/xml/RaceXMLCreator.java +++ b/racevisionGame/src/main/java/mock/xml/RaceXMLCreator.java @@ -103,7 +103,6 @@ public class RaceXMLCreator { } } - /** * Rotate the features in a race such as the boundary, and the marks. * @param race the race to alter diff --git a/racevisionGame/src/main/java/shared/model/Boat.java b/racevisionGame/src/main/java/shared/model/Boat.java index bd3eea5e..edbba025 100644 --- a/racevisionGame/src/main/java/shared/model/Boat.java +++ b/racevisionGame/src/main/java/shared/model/Boat.java @@ -98,6 +98,11 @@ public class Boat extends Collider { */ private boolean sailsOut = true; + /** + * Indicates whether boat is currently involved in a collision + */ + private boolean isColliding = false; + /** * Constructs a boat object with a given sourceID, name, country/team abbreviation, and polars table. * @@ -407,7 +412,7 @@ public class Boat extends Collider { @Override public boolean rayCast(Boat boat) { if(boat != this) { - return rayCast(boat, 100); + return rayCast(boat, 15); } else return false; } @@ -419,4 +424,12 @@ public class Boat extends Collider { notifyObservers(e); } } + + public boolean isColliding() { + return isColliding; + } + + public void setColliding(boolean colliding) { + isColliding = colliding; + } } diff --git a/racevisionGame/src/main/java/shared/model/Constants.java b/racevisionGame/src/main/java/shared/model/Constants.java index c482bafd..6d1c35a8 100644 --- a/racevisionGame/src/main/java/shared/model/Constants.java +++ b/racevisionGame/src/main/java/shared/model/Constants.java @@ -28,7 +28,7 @@ public class Constants { * Frame periods are multiplied by this to get the amount of time a single frame represents. * E.g., frame period = 20ms, scale = 5, frame represents 20 * 5 = 100ms, and so boats are simulated for 100ms, even though only 20ms actually occurred. */ - public static final int RaceTimeScale = 10; + public static final int RaceTimeScale = 2; /** * The race pre-start time, in milliseconds. 30 seconds. diff --git a/racevisionGame/src/main/java/shared/model/Mark.java b/racevisionGame/src/main/java/shared/model/Mark.java index 23778cff..fd46112f 100644 --- a/racevisionGame/src/main/java/shared/model/Mark.java +++ b/racevisionGame/src/main/java/shared/model/Mark.java @@ -29,11 +29,6 @@ public class Mark extends Collider{ */ private GPSCoordinate position; - /** - * Repulsion radius of the mark - */ - private double repulsionRadius = 50; - /** * Constructs a mark with a given source ID, name, and position. * @param sourceID The source ID of the mark. @@ -97,7 +92,7 @@ public class Mark extends Collider{ @Override public boolean rayCast(Boat boat) { - return rayCast(boat, repulsionRadius); + return rayCast(boat, 15); } @Override diff --git a/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/BoatCollisionCommand.java b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/BoatCollisionCommand.java new file mode 100644 index 00000000..271482b5 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/BoatCollisionCommand.java @@ -0,0 +1,46 @@ +package visualiser.Commands.VisualiserRaceCommands; + +import javafx.scene.media.AudioClip; +import mock.model.commandFactory.Command; +import network.Messages.YachtEvent; +import shared.exceptions.BoatNotFoundException; +import visualiser.model.VisualiserBoat; +import visualiser.model.VisualiserRaceState; + +/** + * Created by zwu18 on 4/09/17. + */ +public class BoatCollisionCommand implements Command { + + YachtEvent yachtEvent; + + VisualiserRaceState visualiserRace; + + public BoatCollisionCommand(YachtEvent yachtEvent, VisualiserRaceState visualiserRace){ + this.yachtEvent = yachtEvent; + this.visualiserRace = visualiserRace; + + } + + @Override + public void execute() { + + if(visualiserRace.getPlayerBoatID()==yachtEvent.getSourceID()){ + //System.out.println("I crashed!"); + AudioClip sound = new AudioClip(this.getClass().getResource("/visualiser/sounds/collision.wav").toExternalForm()); + sound.play(); + + } else { + //System.out.println("Someone else crashed!"); + AudioClip sound = new AudioClip(this.getClass().getResource("/visualiser/sounds/quietcollision.wav").toExternalForm()); + sound.play(); + } + + try { + VisualiserBoat boat = visualiserRace.getBoat(yachtEvent.getSourceID()); + boat.setHasCollided(true); + } catch (BoatNotFoundException e) { + e.printStackTrace(); + } + } +} diff --git a/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RaceStatusCommand.java b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RaceStatusCommand.java index 825cd274..add1a3dd 100644 --- a/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RaceStatusCommand.java +++ b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/RaceStatusCommand.java @@ -1,5 +1,6 @@ package visualiser.Commands.VisualiserRaceCommands; +import javafx.scene.media.AudioClip; import mock.model.commandFactory.Command; import network.Messages.BoatStatus; import network.Messages.Enums.BoatStatusEnum; @@ -176,6 +177,10 @@ public class RaceStatusCommand implements Command { //Record order in which boat finished leg. visualiserRace.getLegCompletionOrder().get(boat.getCurrentLeg()).add(boat); + //play sound + AudioClip sound = new AudioClip(getClass().getResource("/visualiser/sounds/passmark.wav").toExternalForm()); + sound.play(); + //Update boat. boat.setCurrentLeg(leg); boat.setTimeAtLastMark(visualiserRace.getRaceClock().getCurrentTime()); diff --git a/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/VisualiserRaceCommandFactory.java b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/VisualiserRaceCommandFactory.java index 37755e91..0e2f3d15 100644 --- a/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/VisualiserRaceCommandFactory.java +++ b/racevisionGame/src/main/java/visualiser/Commands/VisualiserRaceCommands/VisualiserRaceCommandFactory.java @@ -23,7 +23,9 @@ public class VisualiserRaceCommandFactory { switch (message.getType()) { - case BOATLOCATION: return new BoatLocationCommand((BoatLocation) message, visualiserRace); + case BOATLOCATION: + //System.out.println("Boat location received"); + return new BoatLocationCommand((BoatLocation) message, visualiserRace); case RACESTATUS: return new RaceStatusCommand((RaceStatus) message, visualiserRace); @@ -31,6 +33,11 @@ public class VisualiserRaceCommandFactory { case ASSIGN_PLAYER_BOAT: return new AssignPlayerBoatCommand((AssignPlayerBoat) message, visualiserRace); + case YACHTEVENTCODE: + return new BoatCollisionCommand((YachtEvent) message, visualiserRace); + + + default: throw new CommandConstructionException("Could not create VisualiserRaceCommand. Unrecognised or unsupported MessageType: " + message.getType()); } diff --git a/racevisionGame/src/main/java/visualiser/Controllers/InGameLobbyController.java b/racevisionGame/src/main/java/visualiser/Controllers/InGameLobbyController.java index 01151d2b..733d20e0 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/InGameLobbyController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/InGameLobbyController.java @@ -15,6 +15,7 @@ import javafx.scene.control.Label; import javafx.scene.image.ImageView; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; +import javafx.scene.paint.PhongMaterial; import javafx.scene.shape.MeshView; import mock.app.Event; import network.Messages.Enums.RaceStatusEnum; @@ -138,11 +139,15 @@ public class InGameLobbyController extends Controller { playerContainer.add(playerBoatToSet, (count % 3) , row); playerContainer.setMargin(playerBoatToSet, new Insets(10, 10, 10, 10)); - SeaSurface sea = new SeaSurface(750, 200, 250, 0, 210); - subjects.add(sea.getSurface()); + SeaSurface sea = new SeaSurface(750, 200); + sea.setX(250); + sea.setZ(210); + subjects.add(sea); MeshView mesh = new MeshView(importer.getImport()); - Subject3D subject = new Subject3D(mesh); + PhongMaterial boatColorMat = new PhongMaterial(boat.getColor()); + mesh.setMaterial(boatColorMat); + Subject3D subject = new Subject3D(mesh,0); subjects.add(subject); playerBoatToSet.setDistance(50); @@ -159,7 +164,7 @@ public class InGameLobbyController extends Controller { }; rotate.start(); - allPlayerLabels.get(count).setText("Player: " + boat.getSourceID()); + allPlayerLabels.get(count).setText(boat.getName()); allPlayerLabels.get(count).toFront(); count += 1; if (count > 2){ @@ -250,7 +255,7 @@ public class InGameLobbyController extends Controller { visualiserRaceEvent.getVisualiserRaceState().getBoats().removeListener(lobbyUpdateListener); RaceViewController rvc = (RaceViewController) - loadScene("raceView.fxml"); + loadScene("newRaceView.fxml"); rvc.startRace(visualiserRaceEvent, controllerClient, isHost); } catch (IOException e) { diff --git a/racevisionGame/src/main/java/visualiser/Controllers/LobbyController.java b/racevisionGame/src/main/java/visualiser/Controllers/LobbyController.java index 159af2e7..ef8a5350 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/LobbyController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/LobbyController.java @@ -7,6 +7,8 @@ import javafx.scene.control.Button; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.TextField; +import javafx.scene.layout.AnchorPane; +import javafx.scene.media.AudioClip; import network.Messages.HostGame; import visualiser.app.MatchBrowserSingleton; import visualiser.model.RaceConnection; @@ -34,6 +36,8 @@ public class LobbyController extends Controller { private ObservableList connections; private ObservableList customConnections; + private AudioClip sound; + //the socket for match browser private DatagramSocket udpSocket; private MatchBrowserLobbyInterface matchBrowserLobbyInterface; @@ -67,6 +71,8 @@ public class LobbyController extends Controller { * Refreshes the connections in the lobby */ public void refreshBtnPressed(){ + sound = new AudioClip(this.getClass().getResource("/visualiser/sounds/buttonpress.wav").toExternalForm()); + sound.play(); addServerGames(); for(RaceConnection connection: connections) { connection.check(); @@ -92,6 +98,8 @@ public class LobbyController extends Controller { } public void menuBtnPressed() throws IOException { + sound = new AudioClip(this.getClass().getResource("/visualiser/sounds/buttonpress.wav").toExternalForm()); + sound.play(); matchBrowserLobbyInterface.closeSocket(); loadScene("title.fxml"); } @@ -100,6 +108,8 @@ public class LobbyController extends Controller { * adds a new connection */ public void addConnectionPressed(){ + sound = new AudioClip(this.getClass().getResource("/visualiser/sounds/buttonpress.wav").toExternalForm()); + sound.play(); String hostName = addressFld.getText(); String portString = portFld.getText(); try { diff --git a/racevisionGame/src/main/java/visualiser/Controllers/RaceViewController.java b/racevisionGame/src/main/java/visualiser/Controllers/RaceViewController.java index 5bd45274..c2153c1f 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/RaceViewController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/RaceViewController.java @@ -8,27 +8,32 @@ import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.collections.transformation.SortedList; import javafx.fxml.FXML; +import javafx.scene.AmbientLight; +import javafx.scene.PointLight; import javafx.scene.chart.LineChart; import javafx.scene.control.*; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; +import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.scene.paint.Material; +import javafx.scene.paint.PhongMaterial; import javafx.scene.shape.MeshView; -import javafx.scene.shape.Sphere; +import javafx.scene.shape.Shape3D; import javafx.scene.transform.Translate; import javafx.util.Callback; import network.Messages.Enums.RaceStatusEnum; import shared.dataInput.RaceDataSource; -import shared.model.Leg; -import shared.model.Mark; +import shared.exceptions.BoatNotFoundException; +import shared.model.*; import visualiser.app.App; import visualiser.enums.TutorialState; import visualiser.gameController.ControllerClient; import visualiser.gameController.Keys.ControlKey; import visualiser.gameController.Keys.KeyFactory; -import visualiser.layout.Subject3D; -import visualiser.layout.View3D; +import visualiser.layout.*; import visualiser.model.Sparkline; import visualiser.model.VisualiserBoat; import visualiser.model.VisualiserRaceEvent; @@ -61,6 +66,15 @@ public class RaceViewController extends Controller { private View3D view3D; private ObservableList viewSubjects; + /** + * Arrow pointing to next mark in third person + */ + private Subject3D nextMarkArrow; + /** + * Animation loop for rotating mark arrow + */ + private AnimationTimer pointToMark; + // note: it says it's not used but it is! do not remove :) private @FXML ArrowController arrowController; private @FXML GridPane canvasBase; @@ -77,6 +91,8 @@ public class RaceViewController extends Controller { private @FXML TableColumn boatSpeedColumn; private @FXML LineChart sparklineChart; private @FXML Label tutorialText; + private @FXML AnchorPane infoWrapper; + private @FXML AnchorPane lineChartWrapper; /** * Displays a specified race. @@ -121,9 +137,8 @@ public class RaceViewController extends Controller { } } - /** - * Sets up the listener and actions that occur when a key is pressed. - */ + private AnimationTimer arrowToNextMark; + private void initKeypressHandler() { racePane.addEventFilter(KeyEvent.KEY_PRESSED, event -> { String codeString = event.getCode().toString(); @@ -189,10 +204,23 @@ public class RaceViewController extends Controller { * Initialises the various UI components to listen to the {@link #visualiserRace}. */ private void initialiseRaceVisuals() { + + // Import arrow mesh + URL asset = this.getClass().getClassLoader().getResource("assets/arrow V1.0.4.stl"); + StlMeshImporter importer = new StlMeshImporter(); + importer.read(asset); + + MeshView arrow = new MeshView(importer.getImport()); + PhongMaterial arrowMat = new PhongMaterial(Color.RED); + arrow.setMaterial(arrowMat); + + this.nextMarkArrow = new Annotation3D(arrow); + this.nextMarkArrow.setScale(0.1); + // initialise displays initialiseFps(); initialiseInfoTable(); - initialiseView3D(); + initialiseView3D(this.visualiserRace); initialiseRaceClock(); raceTimer(); // start the timer new Sparkline(this.raceState, this.sparklineChart); @@ -200,55 +228,124 @@ public class RaceViewController extends Controller { arrowController.setWindProperty(this.raceState.windProperty()); } - private void initialiseView3D() { + private void initialiseView3D(VisualiserRaceEvent race) { viewSubjects = FXCollections.observableArrayList(); + AmbientLight ambientLight = new AmbientLight(Color.web("#CCCCFF")); + ambientLight.setTranslateX(250); + ambientLight.setTranslateZ(210); + ambientLight.setLightOn(true); + + PointLight pointLight = new PointLight(Color.web("#AAAAFF")); + pointLight.setTranslateX(250); + pointLight.setTranslateZ(210); + pointLight.setLightOn(true); + // Import boat mesh - URL asset = RaceViewController.class.getClassLoader().getResource("assets/V1.2 " + - "Complete Boat.stl"); + URL asset = RaceViewController.class.getClassLoader().getResource("assets/V1.2 Complete Boat.stl"); StlMeshImporter importer = new StlMeshImporter(); importer.read(asset); // Configure camera angles and control - view3D = new View3D(); + URL markerAsset = RaceViewController.class.getClassLoader().getResource("assets/Bouy V1.1.stl"); + StlMeshImporter importerMark = new StlMeshImporter(); + importerMark.read(markerAsset); + + URL alternateBoatAsset = RaceViewController.class.getClassLoader().getResource("assets/V1.3 BurgerBoat.stl"); + StlMeshImporter importerBurgerBoat = new StlMeshImporter(); + importerBurgerBoat.read(alternateBoatAsset); + + view3D = new View3D(false); + view3D.setItems(viewSubjects); view3D.setDistance(1050); - view3D.setYaw(0); - view3D.setPitch(60); + view3D.setBirdsEye(); view3D.enableTracking(); + view3D.addAmbientLight(ambientLight); + view3D.addPointLight(pointLight); canvasBase.add(view3D, 0, 0); // Set up projection from GPS to view - RaceDataSource raceData = raceState.getRaceDataSource(); + RaceDataSource raceData = visualiserRace.getVisualiserRaceState().getRaceDataSource(); final GPSConverter gpsConverter = new GPSConverter(raceData, 450, 450); - view3D.setItems(viewSubjects); + SkyBox skyBox = new SkyBox(750, 200, 250, 0, 210); + viewSubjects.addAll(skyBox.getSkyBoxPlanes()); + + // Set up sea surface + SeaSurface sea = new SeaSurface(750, 200); + sea.setX(250); + sea.setZ(210); + viewSubjects.add(sea); + + // Set up sea surface overlay + SeaSurface seaOverlay = new SeaSurface(4000, 200); + seaOverlay.setX(250); + seaOverlay.setZ(210); + viewSubjects.add(seaOverlay); + + Boundary3D boundary3D = new Boundary3D(visualiserRace.getVisualiserRaceState().getRaceDataSource().getBoundary(), gpsConverter); + for (Subject3D subject3D: boundary3D.getBoundaryNodes()){ + viewSubjects.add(subject3D); + } // Position and add each mark to view - for (Mark mark : raceState.getMarks()) { - Subject3D subject = new Subject3D(new Sphere(2)); - subject.setX(gpsConverter.convertGPS(mark.getPosition()).getX()); - subject.setZ(gpsConverter.convertGPS(mark.getPosition()).getY()); + for(Mark mark: race.getVisualiserRaceState().getMarks()) { + MeshView mesh = new MeshView(importerMark.getImport()); + Subject3D markModel = new Subject3D(mesh, mark.getSourceID()); + + markModel.setX(gpsConverter.convertGPS(mark.getPosition()).getX()); + markModel.setZ(gpsConverter.convertGPS(mark.getPosition()).getY()); - viewSubjects.add(subject); + viewSubjects.add(markModel); } // Position and add each boat to view - for (VisualiserBoat boat : raceState.getBoats()) { - MeshView mesh = new MeshView(importer.getImport()); - Subject3D subject = new Subject3D(mesh); - viewSubjects.add(subject); + for(VisualiserBoat boat: race.getVisualiserRaceState().getBoats()) { + MeshView mesh; + if(boat.getSourceID() == race.getVisualiserRaceState().getPlayerBoatID()) { + mesh = new MeshView(importer.getImport()); + } else { + mesh = new MeshView(importerBurgerBoat.getImport()); + } + PhongMaterial boatColorMat = new PhongMaterial(boat.getColor()); + mesh.setMaterial(boatColorMat); + Subject3D boatModel = new Subject3D(mesh, boat.getSourceID()); + + viewSubjects.add(boatModel); // Track this boat's movement with the new subject AnimationTimer trackBoat = new AnimationTimer() { - @Override public void handle(long now) { - subject.setHeading(boat.getBearing().degrees()); - subject.setX(gpsConverter.convertGPS(boat.getPosition()).getX()); - subject.setZ(gpsConverter.convertGPS(boat.getPosition()).getY()); + @Override + public void handle(long now) { + boatModel.setHeading(boat.getBearing().degrees()); + boatModel.setX(gpsConverter.convertGPS(boat.getPosition()).getX()); + boatModel.setZ(gpsConverter.convertGPS(boat.getPosition()).getY()); } }; trackBoat.start(); + + Material markColor = new PhongMaterial(new Color(0.15,0.9,0.2,1)); + CompoundMark nextMark = boat.getCurrentLeg().getEndCompoundMark(); + view3D.getShape(nextMark.getMark1().getSourceID()).getMesh().setMaterial(markColor); + if(nextMark.getMark2() != null) { + view3D.getShape(nextMark.getMark2().getSourceID()).getMesh().setMaterial(markColor); + } + Subject3D shockwave = new Shockwave(10); + + viewSubjects.add(shockwave); + + boat.legProperty().addListener((o, prev, curr) -> Platform.runLater(() -> swapColours(curr))); + boat.hasCollidedProperty().addListener((o, prev, curr) -> Platform.runLater(() -> showCollision(boat, shockwave))); } // Fix initial bird's-eye position view3D.updatePivot(new Translate(250, 0, 210)); + view3D.targetProperty().addListener((o, prev, curr)-> { + if(curr != null && visualiserRace.getVisualiserRaceState().isVisualiserBoat(curr.getSourceID())) { + addThirdPersonAnnotations(curr); + } else { + removeThirdPersonAnnotations(); + } + }); + // Bind zooming to scrolling view3D.setOnScroll(e -> { view3D.updateDistance(e.getDeltaY()); @@ -257,7 +354,7 @@ public class RaceViewController extends Controller { // Bind zooming to keypress (Z/X default) racePane.addEventFilter(KeyEvent.KEY_PRESSED, e -> { ControlKey key = keyFactory.getKey(e.getCode().toString()); - if (key != null) { + if(key != null) { switch (key.toString()) { case "Zoom In": //Check if race is a tutorial @@ -294,6 +391,95 @@ public class RaceViewController extends Controller { }); } + private void showCollision(VisualiserBoat boat, Subject3D shockwave) { + Subject3D boatModel = view3D.getShape(boat.getSourceID()); + AnimationTimer shockFront = new AnimationTimer() { + double opacity = 1; + + @Override + public void handle(long now) { + shockwave.setX(boatModel.getPosition().getX()); + shockwave.setY(boatModel.getPosition().getY()); + shockwave.setZ(boatModel.getPosition().getZ()); + + if(opacity <= 0) { + shockwave.getMesh().setMaterial(new PhongMaterial(Color.TRANSPARENT)); + boat.setHasCollided(false); + this.stop(); + } + else { + shockwave.getMesh().setMaterial(new PhongMaterial(new Color(1,0,0,opacity))); + opacity -= 0.1; + } + } + }; + shockFront.start(); + } + + private void addThirdPersonAnnotations(Subject3D subject3D) { + viewSubjects.add(nextMarkArrow); + final VisualiserBoat boat; + try { + boat = visualiserRace.getVisualiserRaceState().getBoat(subject3D.getSourceID()); + } catch (BoatNotFoundException e) { + e.printStackTrace(); + return; + } + arrowToNextMark = new AnimationTimer() { + @Override + public void handle(long now) { + CompoundMark target = boat.getCurrentLeg().getEndCompoundMark(); + Bearing headingToMark = GPSCoordinate.calculateBearing(boat.getPosition(), target.getAverageGPSCoordinate()); + + nextMarkArrow.setX(view3D.getPivot().getX()); + nextMarkArrow.setY(view3D.getPivot().getY()); + nextMarkArrow.setZ(view3D.getPivot().getZ() + 15); + nextMarkArrow.setHeading(headingToMark.degrees()); + } + }; + arrowToNextMark.start(); + } + + private void removeThirdPersonAnnotations() { + viewSubjects.remove(nextMarkArrow); + if (arrowToNextMark != null) { + arrowToNextMark.stop(); + } + + } + + /** + * Swap the colour of the next mark to pass with the last mark passed + * @param leg boat has started on + */ + private void swapColours(Leg leg) { + CompoundMark start = leg.getStartCompoundMark(); + CompoundMark end = leg.getEndCompoundMark(); + + //The last leg "finish" doesn't have compound marks. + if (start == null || end == null ) { + return; + } + + Shape3D start1 = view3D.getShape(start.getMark1().getSourceID()).getMesh(); + Shape3D end1 = view3D.getShape(end.getMark1().getSourceID()).getMesh(); + + Material nextMark = start1.getMaterial(); + Material lastMark = end1.getMaterial(); + + start1.setMaterial(lastMark); + if(start.getMark2() != null) { + Shape3D start2 = view3D.getShape(start.getMark2().getSourceID()).getMesh(); + start2.setMaterial(lastMark); + } + + end1.setMaterial(nextMark); + if(end.getMark2() != null) { + Shape3D end2 = view3D.getShape(end.getMark2().getSourceID()).getMesh(); + end2.setMaterial(nextMark); + } + } + /** * Initialises the frame rate functionality. This allows for toggling the * frame rate, and connect the fps label to the race's fps property. @@ -398,6 +584,7 @@ public class RaceViewController extends Controller { /** * Transition from the race view to the finish view. + * @throws IOException Thrown if the finish scene cannot be loaded. */ private void finishRace() throws IOException { RaceFinishController fc = @@ -442,23 +629,7 @@ public class RaceViewController extends Controller { * toggles if the info table is shown */ private void toggleTable() { - double tablePercent = 1 - (boatPlacingColumn.getPrefWidth() + - boatTeamColumn.getPrefWidth() + boatMarkColumn.getPrefWidth() - + boatSpeedColumn.getPrefWidth())/racePane.getWidth(); - - if (infoTableShow) { - racePane.setDividerPositions(tablePercent); - arrowPane.setScaleX(0.5); - arrowPane.setScaleY(0.5); - arrowPane.setTranslateX(0 + (arrowPane.getScene().getWidth()/4)*tablePercent); - arrowPane.setTranslateY(0 - arrowPane.getScene().getHeight()/4); - } else { - racePane.setDividerPositions(1); - arrowPane.setScaleX(1); - arrowPane.setScaleY(1); - arrowPane.setTranslateX(0); - arrowPane.setTranslateY(0); - } + infoWrapper.setVisible(infoTableShow); boatInfoTable.refresh(); infoTableShow = !infoTableShow; } @@ -564,4 +735,4 @@ public class RaceViewController extends Controller { } } -} \ No newline at end of file +} diff --git a/racevisionGame/src/main/java/visualiser/Controllers/TitleController.java b/racevisionGame/src/main/java/visualiser/Controllers/TitleController.java index d051c7de..4f39ea65 100644 --- a/racevisionGame/src/main/java/visualiser/Controllers/TitleController.java +++ b/racevisionGame/src/main/java/visualiser/Controllers/TitleController.java @@ -6,11 +6,20 @@ import javafx.scene.control.RadioButton; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.Pane; +import javafx.scene.media.AudioClip; +import javafx.scene.media.Media; +import javafx.scene.media.MediaPlayer; import javafx.stage.Modality; import mock.exceptions.EventConstructionException; import visualiser.app.App; +import java.io.File; import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ResourceBundle; +import java.util.logging.Level; +import java.util.logging.Logger; /** * Controller for the opening title window. diff --git a/racevisionGame/src/main/java/visualiser/gameController/ControllerServer.java b/racevisionGame/src/main/java/visualiser/gameController/ControllerServer.java index b9dde4af..602f1e33 100644 --- a/racevisionGame/src/main/java/visualiser/gameController/ControllerServer.java +++ b/racevisionGame/src/main/java/visualiser/gameController/ControllerServer.java @@ -80,8 +80,9 @@ public class ControllerServer implements RunnableWithFramePeriod { try { Command command = CommandFactory.createCommand(raceState, boatAction); - compositeCommand.addCommand(command); - + if(command != null) { + compositeCommand.addCommand(command); + } } catch (CommandConstructionException e) { Logger.getGlobal().log(Level.WARNING, "ControllerServer could not create a Command for BoatAction: " + boatAction + ".", e); diff --git a/racevisionGame/src/main/java/visualiser/layout/Annotation3D.java b/racevisionGame/src/main/java/visualiser/layout/Annotation3D.java new file mode 100644 index 00000000..eae706da --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/layout/Annotation3D.java @@ -0,0 +1,24 @@ +package visualiser.layout; + +import javafx.scene.shape.Shape3D; + +/** + * Created by connortaylorbrown on 13/09/17. + */ +public class Annotation3D extends Subject3D { + /** + * Constructor for view subject wrapper + * + * @param mesh to be rendered + */ + public Annotation3D(Shape3D mesh) { + super(mesh, 0); + } + + /** + * Prevent rescaling of this subject + * @param scale ignored + */ + @Override + public void setScale(double scale) {} +} diff --git a/racevisionGame/src/main/java/visualiser/layout/Boundary3D.java b/racevisionGame/src/main/java/visualiser/layout/Boundary3D.java new file mode 100644 index 00000000..35d531c2 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/layout/Boundary3D.java @@ -0,0 +1,89 @@ +package visualiser.layout; + +import com.sun.corba.se.impl.orbutil.graph.Graph; +import javafx.scene.shape.Box; +import javafx.scene.shape.MeshView; +import javafx.scene.shape.Rectangle; +import javafx.scene.shape.Sphere; +import shared.model.GPSCoordinate; +import visualiser.model.GraphCoordinate; +import visualiser.utils.GPSConverter; + +import java.util.ArrayList; +import java.util.List; + +/** + * Class that creates a 3d boundary based on gps coordinates + */ +public class Boundary3D { + + public static double thickness = 1; + private List boundaryNodes = new ArrayList<>(); + private List boundaryConnectors = new ArrayList<>(); + private GPSConverter gpsConverter; + + public Boundary3D(List points, GPSConverter gpsConverter){ + this.gpsConverter = gpsConverter; + setBoundaryNodes(points); + } + + /** + * Splits up the list so that it generates a edge of the boundary + * @param points boundary gpscoordinate + */ + private void setBoundaryNodes(List points){ + if (points.size() < 2){ + return; + } + + for (int i = 0; i < points.size(); i++){ + if (i + 1 != points.size()){ + addBound(points.get(i), points.get(i + 1)); + } else { + addBound(points.get(i), points.get(0)); + } + } + } + + /** + * Add a two point boundary this will create a sphere at coord1 and a line to coord 2 + * this is to reduce double up (2 spheres in one point). + * @param coord1 point to make sphere and start the line. + * @param coord2 point to end the line. + */ + private void addBound(GPSCoordinate coord1, GPSCoordinate coord2){ + GraphCoordinate graphCoord1 = gpsConverter.convertGPS(coord1); + GraphCoordinate graphCoord2 = gpsConverter.convertGPS(coord2); + GraphCoordinate avgCoord = new GraphCoordinate((graphCoord1.getX() + graphCoord2.getX()) / 2, + (graphCoord1.getY() + graphCoord2.getY()) / 2); + + double a = (graphCoord1.getX() - graphCoord2.getX()); + double b = (graphCoord1.getY() - graphCoord2.getY()); + double c = Math.sqrt(a * a + b * b); + + + Subject3D bound1 = new Annotation3D(new Sphere(thickness * 2)); + bound1.setX(graphCoord1.getX()); + bound1.setZ(graphCoord1.getY()); + boundaryNodes.add(bound1); + + Subject3D connector = new Annotation3D(new Box(c, thickness, thickness)); + connector.setX(avgCoord.getX()); + connector.setZ(avgCoord.getY()); + double angle = 90 + Math.toDegrees(GPSConverter.getAngle(graphCoord2, graphCoord1)); + connector.setHeading(angle); + + boundaryConnectors.add(connector); + } + + /** + * get the 3d objects to draw + * @return 3d boundary to draw + */ + public List getBoundaryNodes(){ + //these two must be concatenated with nodes after connectors else the spheres will not overlap the lines + ArrayList result = new ArrayList<>(boundaryConnectors); + result.addAll(boundaryNodes); + return result; + } +} diff --git a/racevisionGame/src/main/java/visualiser/layout/Plane3D.java b/racevisionGame/src/main/java/visualiser/layout/Plane3D.java index 572f3ec5..ca798789 100644 --- a/racevisionGame/src/main/java/visualiser/layout/Plane3D.java +++ b/racevisionGame/src/main/java/visualiser/layout/Plane3D.java @@ -1,5 +1,13 @@ package visualiser.layout; +import com.sun.javafx.geom.PickRay; +import com.sun.javafx.scene.input.PickResultChooser; +import com.sun.javafx.sg.prism.NGNode; +import javafx.scene.Node; +import javafx.scene.shape.*; + +import java.util.ArrayList; +import java.util.Arrays; import javafx.scene.shape.TriangleMesh; import java.util.ArrayList; @@ -29,13 +37,13 @@ public class Plane3D extends TriangleMesh{ for (float l = 0; l <= length; l += subLength) { for (float w = 0; w <= width; w += subWidth){ - //add points - pointsList.add(w + startW); - pointsList.add(l + startL); - pointsList.add(0f); - //addTexture coords - textureCoord.add(1 - w/width); - textureCoord.add(1 - l/length); + //add points + pointsList.add(w + startW); + pointsList.add(l + startL); + pointsList.add(0f); + //addTexture coords + textureCoord.add(1 - w/width); + textureCoord.add(1 - l/length); } } @@ -92,6 +100,7 @@ public class Plane3D extends TriangleMesh{ float x = this.getPoints().get(i); float y = this.getPoints().get(i + 1); float z = this.getPoints().get(i + 2); + System.out.println(String.format("Point at %d is x:%f, y:%f, z:%f", index, x, y, z)); } /** @@ -121,4 +130,4 @@ public class Plane3D extends TriangleMesh{ } -} \ No newline at end of file +} diff --git a/racevisionGame/src/main/java/visualiser/layout/SeaSurface.java b/racevisionGame/src/main/java/visualiser/layout/SeaSurface.java index 7c9a1889..af3374fb 100644 --- a/racevisionGame/src/main/java/visualiser/layout/SeaSurface.java +++ b/racevisionGame/src/main/java/visualiser/layout/SeaSurface.java @@ -1,65 +1,67 @@ package visualiser.layout; +import javafx.geometry.Point3D; import javafx.scene.image.Image; +import javafx.scene.image.ImageView; import javafx.scene.image.PixelWriter; import javafx.scene.image.WritableImage; import javafx.scene.paint.Color; import javafx.scene.paint.PhongMaterial; +import javafx.scene.shape.Box; import javafx.scene.shape.MeshView; +import javafx.scene.shape.Shape3D; +import javafx.scene.shape.TriangleMesh; import visualiser.utils.PerlinNoiseGenerator; /** * Creates a SeaSurface */ -public class SeaSurface { - private float[][] noiseArray; - private Subject3D surface; +public class SeaSurface extends Subject3D { + private static Image image; /** * Sea Surface Constructor + * * @param size size of the sea surface (has to be square for simplicity's sake) * @param freq frequency the perlin noise is to be generated at - * @param x offset that the sea should be set at position-wise - * @param y offset that the sea should be set at position-wise - * @param z offset that the sea should be set at position-wise */ - public SeaSurface(int size, double freq, double x, double y, double z){ - noiseArray = PerlinNoiseGenerator.createNoise(size, freq); - createSurface(); - surface.setZ(z); - surface.setY(y); - surface.setX(x); + public SeaSurface(int size, double freq) { + super(createSurface(size, freq), 0); + image = new Image(getClass().getClassLoader().getResourceAsStream("images/skybox/ThickCloudsWaterDown2048.png")); } /** * Creates the sea surface + * @param size size of sea noise array. + * @param freq frequency of sea noise array. + * @return The sea surface. */ - private void createSurface(){ - Image diffuseMap = createImage(noiseArray.length, noiseArray); + private static Shape3D createSurface(int size, double freq) { + float[][] noiseArray = PerlinNoiseGenerator.createNoise(size, freq); + Image diffuseMap = createImage(noiseArray.length, noiseArray); //image PhongMaterial material = new PhongMaterial(); + material.setDiffuseColor(Color.web("#FFFFFF")); + material.setSpecularColor(Color.web("#000000")); material.setDiffuseMap(diffuseMap); - //material.setSpecularColor(Color.WHITE); Plane3D seaPlane = new Plane3D(noiseArray.length, noiseArray.length, 10, 10); MeshView seaSurface = new MeshView(seaPlane); -// Box seaSurface = new Box(noiseArray.length, 0.1, noiseArray.length); seaSurface.setMaterial(material); seaSurface.setMouseTransparent(true); seaSurface.toFront(); - //seaSurface.setRotationAxis(new Point3D(1, 0, 0)); - //seaSurface.setRotate(90); - surface = new Subject3D(seaSurface); + return seaSurface; } /** * Create texture for uv mapping - * @param size size of the image to make + * + * @param size size of the image to make * @param noise array of noise * @return image that is created */ - private Image createImage(double size, float[][] noise) { + private static Image createImage(double size, float[][] noise) { int width = (int) size; int height = (int) size; @@ -94,9 +96,10 @@ public class SeaSurface { /** * Nomalises the values so that the colours are correct - * @param value value to normalise - * @param min current min - * @param max current max + * + * @param value value to normalise + * @param min current min + * @param max current max * @param newMin new min * @param newMax new max * @return returns normalised value @@ -109,9 +112,10 @@ public class SeaSurface { /** * clamps a value between a min and max + * * @param value value to clamp - * @param min minimum value it can be - * @param max maximum value it can be + * @param min minimum value it can be + * @param max maximum value it can be * @return result after clamp */ private static double clamp(double value, double min, double max) { @@ -126,11 +130,11 @@ public class SeaSurface { } /** - * Get surface - * @return the surface so it can be drawn + * Prevent rescaling of sea surface + * + * @param scale ignored */ - public Subject3D getSurface(){ - return surface; + @Override + public void setScale(double scale) { } - -} \ No newline at end of file +} diff --git a/racevisionGame/src/main/java/visualiser/layout/Shockwave.java b/racevisionGame/src/main/java/visualiser/layout/Shockwave.java new file mode 100644 index 00000000..efdb0c5d --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/layout/Shockwave.java @@ -0,0 +1,17 @@ +package visualiser.layout; + +import javafx.scene.paint.Color; +import javafx.scene.paint.PhongMaterial; +import javafx.scene.shape.Cylinder; +import javafx.scene.transform.Rotate; + +/** + * Created by cbt24 on 14/09/17. + */ +public class Shockwave extends Subject3D { + public Shockwave(double radius) { + super(new Cylinder(radius,0),0); + getMesh().getTransforms().add(new Rotate(-90, Rotate.X_AXIS)); + getMesh().setMaterial(new PhongMaterial(new Color(0,0,0,0))); + } +} diff --git a/racevisionGame/src/main/java/visualiser/layout/SkyBox.java b/racevisionGame/src/main/java/visualiser/layout/SkyBox.java new file mode 100644 index 00000000..bef03170 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/layout/SkyBox.java @@ -0,0 +1,163 @@ +package visualiser.layout; + +import javafx.geometry.Point3D; +import javafx.scene.image.Image; +import javafx.scene.paint.Color; +import javafx.scene.paint.PhongMaterial; +import javafx.scene.shape.MeshView; +import javafx.scene.transform.Rotate; + +import java.util.ArrayList; +import java.util.List; + +/** + * Creates a skyBox + */ +public class SkyBox { + private int size; + private double x; + private double y; + private double z; + private double freq; + private double yshift; + private double clipOverlap; + private List skyBoxPlanes = new ArrayList<>(); + + public SkyBox(int edgeLength, double freq, double x, double y, double z) { + this.size = edgeLength; + this.x = x; + this.y = y; + this.z = z; + this.freq = freq; + this.yshift = -size/64; + clipOverlap = 0; + makeSkyBox(); + } + + private void makeSkyBox() { + addTop(); + addFront(); + addBack(); + addLeft(); + addRight(); + //addSeaOverlay(); + } + + private void addTop() { + MeshView surface = makeSurface(new Image(getClass().getClassLoader().getResourceAsStream("images/skybox/ThickCloudsWaterUp2048.png")), size); + + surface.setRotationAxis(new Point3D(0, 0, 1)); + surface.setRotate(180); + + surface.setTranslateX(x); + surface.setTranslateY(y - size + 1); + surface.setTranslateZ(z); + + Subject3D top = new SkyBoxPlane(surface,0); + skyBoxPlanes.add(top); + } + + private void addRight() { + MeshView surface = makeSurface(new Image(getClass().getClassLoader().getResourceAsStream("images/skybox/ThickCloudsWaterRight2048.png")), size + 1); + + surface.setTranslateX(size/2); + surface.setTranslateY(size/2); + surface.setRotationAxis(new Point3D(1, 0, 0)); + surface.setRotate(90); + surface.setTranslateX(-size/2); + surface.setTranslateY(-size/2); + + surface.setTranslateX(x); + surface.setTranslateY(y + yshift); + surface.setTranslateZ(z + size/2 - clipOverlap); + + + Subject3D right = new SkyBoxPlane(surface,0); + skyBoxPlanes.add(right); + } + + private void addLeft() { + MeshView surface = makeSurface(new Image(getClass().getClassLoader().getResourceAsStream("images/skybox/ThickCloudsWaterLeft2048.png")), size + 1); + + surface.setTranslateX(size/2); + surface.setTranslateY(size/2); + surface.setRotationAxis(new Point3D(1, 0, 0)); + surface.setRotate(-90); + surface.setTranslateX(-size/2); + surface.setTranslateY(-size/2); + + surface.setScaleX(-1); + surface.setScaleZ(-1); + + surface.setTranslateX(x); + surface.setTranslateY(y + yshift); + surface.setTranslateZ(z - size/2 + clipOverlap); + + + Subject3D left = new SkyBoxPlane(surface,0); + skyBoxPlanes.add(left); + } + + private void addBack() { + MeshView surface = makeSurface(new Image(getClass().getClassLoader().getResourceAsStream("images/skybox/ThickCloudsWaterBack2048.png")), size); + surface.getTransforms().add(new Rotate(90, 0, 0)); + + surface.setRotationAxis(new Point3D(1, 0, 0)); + surface.setRotate(-90); + + surface.setScaleY(-1); + surface.setScaleZ(-1); + + surface.setTranslateX(x - size/2 + clipOverlap); + surface.setTranslateY(y + yshift); + surface.setTranslateZ(z); + + Subject3D back = new SkyBoxPlane(surface,0); + skyBoxPlanes.add(back); + } + + private void addFront() { + MeshView surface = makeSurface(new Image(getClass().getClassLoader().getResourceAsStream("images/skybox/ThickCloudsWaterFront2048.png")), size); + + surface.setTranslateX(size/2); + surface.setTranslateY(size/2); + surface.setRotationAxis(new Point3D(0, 0, 1)); + surface.setRotate(-90); + surface.setTranslateX(-size/2); + surface.setTranslateY(-size/2); + + surface.setTranslateX(x + size/2 - clipOverlap); + surface.setTranslateY(y + yshift); + surface.setTranslateZ(z); + + Subject3D front = new SkyBoxPlane(surface,0); + skyBoxPlanes.add(front); + } + + private MeshView makeSurface(Image diffuseMap, int size) { + + PhongMaterial material = new PhongMaterial(); + material.setDiffuseColor(Color.web("#FFFFFF")); + material.setSpecularColor(Color.web("#000000")); + material.setDiffuseMap(diffuseMap); + + Plane3D plane = new Plane3D(size, size, 10, 10); + MeshView surface = new MeshView(plane); + surface.setMaterial(material); + surface.setMouseTransparent(true); + return surface; + } + + private void addSeaOverlay() { + SeaSurface seaSurface = new SeaSurface(size * 3, freq); + seaSurface.setX(x); + seaSurface.setY(y - size/4 + 1); + seaSurface.setZ(z); + skyBoxPlanes.add(seaSurface); + } + + public List getSkyBoxPlanes() { + return skyBoxPlanes; + } +} + diff --git a/racevisionGame/src/main/java/visualiser/layout/SkyBoxPlane.java b/racevisionGame/src/main/java/visualiser/layout/SkyBoxPlane.java new file mode 100644 index 00000000..30858b66 --- /dev/null +++ b/racevisionGame/src/main/java/visualiser/layout/SkyBoxPlane.java @@ -0,0 +1,14 @@ +package visualiser.layout; + +import javafx.scene.shape.Shape3D; + +public class SkyBoxPlane extends Subject3D { + + public SkyBoxPlane(Shape3D mesh, int sourceID) { + super(mesh,sourceID); + } + + @Override + public void setScale(double scale) { + } +} diff --git a/racevisionGame/src/main/java/visualiser/layout/Subject3D.java b/racevisionGame/src/main/java/visualiser/layout/Subject3D.java index af76f4f4..e6a6f047 100644 --- a/racevisionGame/src/main/java/visualiser/layout/Subject3D.java +++ b/racevisionGame/src/main/java/visualiser/layout/Subject3D.java @@ -2,6 +2,7 @@ package visualiser.layout; import javafx.scene.shape.Shape3D; import javafx.scene.transform.Rotate; +import javafx.scene.transform.Scale; import javafx.scene.transform.Translate; /** @@ -12,6 +13,10 @@ public class Subject3D { * Rendered mesh */ private Shape3D mesh; + /** + * Source ID of subject in game model + */ + private int sourceID; /** * Position translation updated by state listeners @@ -23,22 +28,30 @@ public class Subject3D { */ private Rotate heading; + private Scale scale; + /** * Constructor for view subject wrapper * @param mesh to be rendered + * @param sourceID Source ID of the subject. */ - public Subject3D(Shape3D mesh) { + public Subject3D(Shape3D mesh, int sourceID) { this.mesh = mesh; + this.sourceID = sourceID; + this.scale = new Scale(); this.position = new Translate(); this.heading = new Rotate(0, Rotate.Y_AXIS); - - this.mesh.getTransforms().addAll(position, heading, new Rotate(90, Rotate.X_AXIS), new Rotate(180, Rotate.Y_AXIS)); + this.mesh.getTransforms().addAll(position, scale, heading, new Rotate(90, Rotate.X_AXIS), new Rotate(180, Rotate.Y_AXIS)); } public Shape3D getMesh() { return mesh; } + public int getSourceID() { + return sourceID; + } + public Translate getPosition() { return this.position; } @@ -47,6 +60,12 @@ public class Subject3D { return heading; } + public void setScale(double scale) { + this.scale.setX(scale); + this.scale.setY(scale); + this.scale.setZ(scale); + } + public void setX(double x) { position.setX(x); } diff --git a/racevisionGame/src/main/java/visualiser/layout/View3D.java b/racevisionGame/src/main/java/visualiser/layout/View3D.java index 6b5db309..e9069c5d 100644 --- a/racevisionGame/src/main/java/visualiser/layout/View3D.java +++ b/racevisionGame/src/main/java/visualiser/layout/View3D.java @@ -1,11 +1,12 @@ package visualiser.layout; +import javafx.animation.AnimationTimer; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ChangeListener; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; -import javafx.scene.Group; -import javafx.scene.PerspectiveCamera; -import javafx.scene.SubScene; +import javafx.scene.*; import javafx.scene.input.PickResult; import javafx.scene.layout.Pane; import javafx.scene.paint.Color; @@ -33,11 +34,15 @@ public class View3D extends Pane { /** * Map for selecting Subject3D from Shape3D */ - private Map selectionMap; + private Map shapeMap; + /** + * Map for selecting Subject3D from source ID + */ + private Map sourceMap; /** * Subject tracked by camera */ - private Subject3D target; + private ObjectProperty target; /** * Rendering container for shapes */ @@ -67,44 +72,43 @@ public class View3D extends Pane { */ private Rotate pitch; /** - * Single listener for subject heading changes - */ - private ChangeListener pivotHeading = (o, prev, curr) -> yaw.setAngle((double)curr); - /** - * Single listener for subject position (x) changes - */ - private ChangeListener pivotX = (o, prev, curr) -> pivot.setX((double)curr); - /** - * Single listener for subject position (y) changes + * Animation loop for camera tracking */ - private ChangeListener pivotY = (o, prev, curr) -> pivot.setY((double)curr); + private AnimationTimer trackBoat; /** - * Single listener for subject position (z) changes + * Distance to switch from third person to bird's eye */ - private ChangeListener pivotZ = (o, prev, curr) -> pivot.setZ((double)curr); + private final double THIRD_PERSON_LIMIT = 100; /** - * Distance to switch from third person to bird's eye + * Distance to stop zoom */ - private double THIRD_PERSON_LIMIT = 100; + private final double FIRST_PERSON_LIMIT = 2; /** * Default constructor for View3D. Sets up Scene and PerspectiveCamera. + * @param fill whether or not to fill the background of the view. */ - public View3D() { + public View3D(boolean fill) { this.world = new Group(); - this.selectionMap = new HashMap<>(); - this.target = null; + this.shapeMap = new HashMap<>(); + this.sourceMap = new HashMap<>(); + this.target = new SimpleObjectProperty<>(null); this.scene = new SubScene(world, 300, 300); scene.widthProperty().bind(this.widthProperty()); scene.heightProperty().bind(this.heightProperty()); - scene.setFill(new Color(0.2, 0.6, 1, 1)); - + if (fill) { + scene.setFill(new Color(0.2, 0.6, 1, 1)); + } scene.setCamera(buildCamera()); this.getChildren().add(scene); } + public View3D(){ + this(true); + } + /** * Sets up camera view frustum and binds transformations * @return perspective camera @@ -140,17 +144,23 @@ public class View3D extends Pane { if (c.wasRemoved() || c.wasAdded()) { for (Subject3D shape : c.getRemoved()) { world.getChildren().remove(shape.getMesh()); - selectionMap.remove(shape.getMesh()); + shapeMap.remove(shape.getMesh()); + sourceMap.remove(shape.getSourceID()); } for (Subject3D shape : c.getAddedSubList()) { world.getChildren().add(shape.getMesh()); - selectionMap.put(shape.getMesh(), shape); + shapeMap.put(shape.getMesh(), shape); + sourceMap.put(shape.getSourceID(), shape); } } } }); } + public Subject3D getShape(int sourceID) { + return sourceMap.get(sourceID); + } + /** * Intercept mouse clicks on subjects in view. The applied listener cannot be removed. */ @@ -158,20 +168,48 @@ public class View3D extends Pane { scene.setOnMousePressed(e -> { PickResult result = e.getPickResult(); if(result != null && result.getIntersectedNode() != null && result.getIntersectedNode() instanceof Shape3D) { - trackSubject(selectionMap.get(result.getIntersectedNode())); + untrackSubject(); + trackSubject(shapeMap.get(result.getIntersectedNode())); + setThirdPerson(); } }); } + public ObjectProperty targetProperty() { + return target; + } + + /** + * Configures camera to third person view + */ + public void setThirdPerson() { + this.setDistance(THIRD_PERSON_LIMIT / 2); + this.setPitch(10); + + for(Subject3D item: items) { + item.setScale(0.1); + } + } + + /** + * Configures camera to bird's eye view + */ + public void setBirdsEye() { + this.setYaw(0); + this.setPitch(60); + + for(Subject3D item: items) { + item.setScale(1); + } + } + /** * Stop camera from following the last selected subject */ private void untrackSubject() { - if(target != null) { - target.getPosition().xProperty().removeListener(pivotX); - target.getPosition().yProperty().removeListener(pivotY); - target.getPosition().zProperty().removeListener(pivotZ); - target.getHeading().angleProperty().removeListener(pivotHeading); + if(target.get() != null) { + trackBoat.stop(); + target.setValue(null); } } @@ -180,19 +218,16 @@ public class View3D extends Pane { * @param subject to track */ private void trackSubject(Subject3D subject) { - untrackSubject(); - target = subject; - - updatePivot(target.getPosition()); - setYaw(target.getHeading().getAngle()); - - target.getPosition().xProperty().addListener(pivotX); - target.getPosition().yProperty().addListener(pivotY); - target.getPosition().zProperty().addListener(pivotZ); - target.getHeading().angleProperty().addListener(pivotHeading); + target.set(subject); - this.setDistance(THIRD_PERSON_LIMIT); - this.setPitch(20); + this.trackBoat = new AnimationTimer() { + @Override + public void handle(long now) { + updatePivot(target.get().getPosition()); + setYaw(target.get().getHeading().getAngle()); + } + }; + trackBoat.start(); } public void setNearClip(double nearClip) { @@ -203,6 +238,10 @@ public class View3D extends Pane { this.farClip = farClip; } + public Translate getPivot() { + return pivot; + } + /** * Sets the coordinates of the camera pivot once. * @param pivot source of coordinates @@ -230,13 +269,12 @@ public class View3D extends Pane { public void updateDistance(double delta) { double distance = -this.distance.getZ() + delta; - if(distance <= 0) { - this.setDistance(0); + if(distance <= FIRST_PERSON_LIMIT) { + this.setDistance(FIRST_PERSON_LIMIT); } else if(distance > THIRD_PERSON_LIMIT) { - untrackSubject(); - this.setYaw(0); - this.setPitch(60); this.setDistance(distance); + untrackSubject(); + setBirdsEye(); } else { this.setDistance(distance); } @@ -257,4 +295,12 @@ public class View3D extends Pane { public void setPitch(double pitch) { this.pitch.setAngle(-pitch); } + + public void addAmbientLight(AmbientLight ambientLight) { + this.world.getChildren().add(ambientLight); + } + + public void addPointLight(PointLight pointLight) { + this.world.getChildren().add(pointLight); + } } \ No newline at end of file diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java b/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java index 12e2ee37..1184197c 100644 --- a/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java +++ b/racevisionGame/src/main/java/visualiser/model/VisualiserBoat.java @@ -1,6 +1,8 @@ package visualiser.model; +import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.scene.paint.Color; import network.Messages.Enums.BoatStatusEnum; @@ -62,6 +64,7 @@ public class VisualiserBoat extends Boat { private ObjectProperty positionProperty; private ObjectProperty bearingProperty; + private BooleanProperty hasCollided; /** @@ -74,6 +77,7 @@ public class VisualiserBoat extends Boat { super(boat.getSourceID(), boat.getName(), boat.getCountry()); this.color = color; + this.hasCollided = new SimpleBooleanProperty(false); } @@ -253,10 +257,6 @@ public class VisualiserBoat extends Boat { this.positionProperty.set(position); } - public ObjectProperty positionProperty() { - return positionProperty; - } - @Override public Bearing getBearing() { return bearingProperty.get(); @@ -270,7 +270,15 @@ public class VisualiserBoat extends Boat { this.bearingProperty.set(bearing); } - public ObjectProperty bearingProperty() { - return bearingProperty; + public boolean hasCollided() { + return hasCollided.get(); + } + + public BooleanProperty hasCollidedProperty() { + return hasCollided; + } + + public void setHasCollided(boolean hasCollided) { + this.hasCollided.set(hasCollided); } } diff --git a/racevisionGame/src/main/java/visualiser/model/VisualiserRaceController.java b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceController.java index 748808b3..47382b74 100644 --- a/racevisionGame/src/main/java/visualiser/model/VisualiserRaceController.java +++ b/racevisionGame/src/main/java/visualiser/model/VisualiserRaceController.java @@ -65,7 +65,7 @@ public class VisualiserRaceController implements RunnableWithFramePeriod { compositeRaceCommand.addCommand(command); } catch (CommandConstructionException e) { - Logger.getGlobal().log(Level.WARNING, "VisualiserRaceController could not create a command for incoming message."); + //Logger.getGlobal().log(Level.WARNING, "VisualiserRaceController could not create a command for incoming message."); } catch (InterruptedException e) { Logger.getGlobal().log(Level.SEVERE, "VisualiserRaceController was interrupted on thread: " + Thread.currentThread() + " while waiting for messages."); diff --git a/racevisionGame/src/main/java/visualiser/utils/GPSConverter.java b/racevisionGame/src/main/java/visualiser/utils/GPSConverter.java index 22dd937f..542b043f 100644 --- a/racevisionGame/src/main/java/visualiser/utils/GPSConverter.java +++ b/racevisionGame/src/main/java/visualiser/utils/GPSConverter.java @@ -101,4 +101,14 @@ public class GPSConverter { return convertGPS(coordinate.getLatitude(), coordinate.getLongitude()); } + /** + * Gets the bearing between two coordinates + * @param coord1 coordinate 1 + * @param coord2 coordinate 2 + * @return return the bearing between the two. + */ + public static double getAngle(GraphCoordinate coord1, GraphCoordinate coord2){ + return Math.atan2(coord2.getX() - coord1.getX(), coord2.getY() - coord1.getY()); + } + } diff --git a/racevisionGame/src/main/java/visualiser/utils/PerlinNoiseGenerator.java b/racevisionGame/src/main/java/visualiser/utils/PerlinNoiseGenerator.java index becf59a2..00167656 100644 --- a/racevisionGame/src/main/java/visualiser/utils/PerlinNoiseGenerator.java +++ b/racevisionGame/src/main/java/visualiser/utils/PerlinNoiseGenerator.java @@ -85,4 +85,4 @@ public class PerlinNoiseGenerator { static { for (int i=0; i < 256 ; i++) p[256+i] = p[i] = permutation[i]; } } -} \ No newline at end of file +} diff --git a/racevisionGame/src/main/resources/assets/Bouy V1.1.stl b/racevisionGame/src/main/resources/assets/Bouy V1.1.stl new file mode 100644 index 00000000..3ba11615 Binary files /dev/null and b/racevisionGame/src/main/resources/assets/Bouy V1.1.stl differ diff --git a/racevisionGame/src/main/resources/assets/V1.3 BurgerBoat.stl b/racevisionGame/src/main/resources/assets/V1.3 BurgerBoat.stl new file mode 100644 index 00000000..b8f0f54a Binary files /dev/null and b/racevisionGame/src/main/resources/assets/V1.3 BurgerBoat.stl differ diff --git a/racevisionGame/src/main/resources/assets/arrow V1.0.4.stl b/racevisionGame/src/main/resources/assets/arrow V1.0.4.stl new file mode 100644 index 00000000..3b7d1a6f Binary files /dev/null and b/racevisionGame/src/main/resources/assets/arrow V1.0.4.stl differ diff --git a/racevisionGame/src/main/resources/css/dayMode.css b/racevisionGame/src/main/resources/css/dayMode.css index b62f8391..39903bdb 100644 --- a/racevisionGame/src/main/resources/css/dayMode.css +++ b/racevisionGame/src/main/resources/css/dayMode.css @@ -89,3 +89,15 @@ -fx-focus-color: transparent; -fx-background-color: transparent; } + +#lineChartWrapper{ + -fx-border-color: #02378c; + -fx-background-color: #4783e0; + -fx-border-width: 3; +} + +#boatInfoTable{ + -fx-border-color: #012256; + -fx-border-width: 3; +} + diff --git a/racevisionGame/src/main/resources/css/nightMode.css b/racevisionGame/src/main/resources/css/nightMode.css index deefa51a..514893aa 100644 --- a/racevisionGame/src/main/resources/css/nightMode.css +++ b/racevisionGame/src/main/resources/css/nightMode.css @@ -91,3 +91,14 @@ -fx-focus-color: transparent; -fx-background-color: transparent; } + +#lineChartWrapper{ + -fx-border-color: #02378c; + -fx-background-color: #012256; + -fx-border-width: 3; +} + +#boatInfoTable{ + -fx-border-color: #012256; + -fx-border-width: 3; +} diff --git a/racevisionGame/src/main/resources/images/skybox/ThickCloudsWaterBack2048.png b/racevisionGame/src/main/resources/images/skybox/ThickCloudsWaterBack2048.png new file mode 100644 index 00000000..833d3eb2 Binary files /dev/null and b/racevisionGame/src/main/resources/images/skybox/ThickCloudsWaterBack2048.png differ diff --git a/racevisionGame/src/main/resources/images/skybox/ThickCloudsWaterDown2048.png b/racevisionGame/src/main/resources/images/skybox/ThickCloudsWaterDown2048.png new file mode 100644 index 00000000..8c1572e0 Binary files /dev/null and b/racevisionGame/src/main/resources/images/skybox/ThickCloudsWaterDown2048.png differ diff --git a/racevisionGame/src/main/resources/images/skybox/ThickCloudsWaterFront2048.png b/racevisionGame/src/main/resources/images/skybox/ThickCloudsWaterFront2048.png new file mode 100644 index 00000000..0d9b4f52 Binary files /dev/null and b/racevisionGame/src/main/resources/images/skybox/ThickCloudsWaterFront2048.png differ diff --git a/racevisionGame/src/main/resources/images/skybox/ThickCloudsWaterLeft2048.png b/racevisionGame/src/main/resources/images/skybox/ThickCloudsWaterLeft2048.png new file mode 100644 index 00000000..d3fe44ad Binary files /dev/null and b/racevisionGame/src/main/resources/images/skybox/ThickCloudsWaterLeft2048.png differ diff --git a/racevisionGame/src/main/resources/images/skybox/ThickCloudsWaterRight2048.png b/racevisionGame/src/main/resources/images/skybox/ThickCloudsWaterRight2048.png new file mode 100644 index 00000000..09444f15 Binary files /dev/null and b/racevisionGame/src/main/resources/images/skybox/ThickCloudsWaterRight2048.png differ diff --git a/racevisionGame/src/main/resources/images/skybox/ThickCloudsWaterUp2048.png b/racevisionGame/src/main/resources/images/skybox/ThickCloudsWaterUp2048.png new file mode 100644 index 00000000..6c501e95 Binary files /dev/null and b/racevisionGame/src/main/resources/images/skybox/ThickCloudsWaterUp2048.png differ diff --git a/racevisionGame/src/main/resources/images/skybox1/skyBack.png b/racevisionGame/src/main/resources/images/skybox1/skyBack.png new file mode 100644 index 00000000..390ff4e8 Binary files /dev/null and b/racevisionGame/src/main/resources/images/skybox1/skyBack.png differ diff --git a/racevisionGame/src/main/resources/images/skybox1/skyFront.png b/racevisionGame/src/main/resources/images/skybox1/skyFront.png new file mode 100644 index 00000000..50769c3d Binary files /dev/null and b/racevisionGame/src/main/resources/images/skybox1/skyFront.png differ diff --git a/racevisionGame/src/main/resources/images/skybox1/skyLeft.png b/racevisionGame/src/main/resources/images/skybox1/skyLeft.png new file mode 100644 index 00000000..9ee2c8b7 Binary files /dev/null and b/racevisionGame/src/main/resources/images/skybox1/skyLeft.png differ diff --git a/racevisionGame/src/main/resources/images/skybox1/skyRight.png b/racevisionGame/src/main/resources/images/skybox1/skyRight.png new file mode 100644 index 00000000..df386ef8 Binary files /dev/null and b/racevisionGame/src/main/resources/images/skybox1/skyRight.png differ diff --git a/racevisionGame/src/main/resources/images/skybox1/skyTop.png b/racevisionGame/src/main/resources/images/skybox1/skyTop.png new file mode 100644 index 00000000..90e33729 Binary files /dev/null and b/racevisionGame/src/main/resources/images/skybox1/skyTop.png differ diff --git a/racevisionGame/src/main/resources/mock/mockXML/raceThreePlayers.xml b/racevisionGame/src/main/resources/mock/mockXML/raceThreePlayers.xml new file mode 100644 index 00000000..c4efcd17 --- /dev/null +++ b/racevisionGame/src/main/resources/mock/mockXML/raceThreePlayers.xml @@ -0,0 +1,51 @@ + + + 5326 + FLEET + RACE_CREATION_TIME + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/racevisionGame/src/main/resources/visualiser/scenes/newRaceView.fxml b/racevisionGame/src/main/resources/visualiser/scenes/newRaceView.fxml new file mode 100644 index 00000000..76195416 --- /dev/null +++ b/racevisionGame/src/main/resources/visualiser/scenes/newRaceView.fxml @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +