Merge branch 'master' into new_3d_assets

# Conflicts:
#	racevisionGame/src/main/java/visualiser/Controllers/RaceViewController.java
This commit is contained in:
Fan-Wu Yang
2017-09-28 00:57:06 +13:00
43 changed files with 1029 additions and 148 deletions
@@ -40,24 +40,17 @@ public class Event {
private String raceXML;
private String regattaXML;
private String boatXML;
private XMLFileType xmlFileType;
private Polars boatPolars;
/**
* Data sources containing data from the xml files.
*/
RaceDataSource raceDataSource;
BoatDataSource boatDataSource;
RegattaDataSource regattaDataSource;
private RaceDataSource raceDataSource;
private BoatDataSource boatDataSource;
private RegattaDataSource regattaDataSource;
private ConnectionAcceptor connectionAcceptor;
private LatestMessages latestMessages;
private CompositeCommand compositeCommand;
/**
* This is used to allocate source IDs.
*/
@@ -66,22 +59,18 @@ public class Event {
private RaceLogic raceLogic;
private Thread raceThread;
private Thread connectionThread;
private int mapIndex;
/**
* Constructs an event, using various XML files.
* @param singlePlayer Whether or not to create a single player event.
* @param mapIndex Specifies which map to use.
* @param raceLength The length of the race, in milliseconds.
* @throws EventConstructionException Thrown if we cannot create an Event for any reason.
*/
public Event(boolean singlePlayer, int mapIndex) throws EventConstructionException {
public Event(boolean singlePlayer, int mapIndex, int raceLength) throws
EventConstructionException {
PolarParser.parseNewPolars("mock/polars/acc_polars.csv");
this.mapIndex = mapIndex;
String raceXMLFile;
@@ -125,8 +114,8 @@ public class Event {
} else {
this.raceXML = Event.setRaceXMLAtCurrentTimeToNow(XMLReader.readXMLFileToString(raceXMLFile, StandardCharsets.UTF_8));
this.raceXML = RaceXMLCreator.alterRaceToWind(this.raceXML, XMLFileType.Contents, windAngle);
this.raceXML = RaceXMLCreator.scaleRaceSize(raceXML, windSpeed, 15 * 60 * 1000);
this.raceXML = RaceXMLCreator.scaleRaceSize(raceXML,
windSpeed, raceLength);
}
this.boatXML = XMLReader.readXMLFileToString(boatsXMLFile, StandardCharsets.UTF_8);
@@ -27,7 +27,7 @@ public class MockBoat extends Boat {
* 1: passed only first check
* 2: passed first and second check
*/
private Integer roundingStatus = 0;
private int roundingStatus = 0;
/**
* Stores whether the boat is on autoVMG or not
@@ -284,8 +284,8 @@ public class MockBoat extends Boat {
(this.isStarboardSide(mark1) && this.isPortSide(mark2));
}
public Integer getRoundingStatus() {
return Integer.valueOf(roundingStatus);
public int getRoundingStatus() {
return roundingStatus;
}
public void increaseRoundingStatus() {
@@ -419,6 +419,11 @@ public class MockRace extends RaceState {
boat.setTimeSinceTackChange(boat.getTimeSinceTackChange() + updatePeriodMilliseconds);
}
// Remove one unit of health for every frame spent outside boundary
if(!finish && !GPSCoordinate.isInsideBoundary(boat.getPosition(), getBoundary())) {
boat.updateHealth(-0.1);
}
this.updateEstimatedTime(boat);
}
@@ -430,7 +435,7 @@ public class MockRace extends RaceState {
boat.getBearing()
), boat.getBearing()) ;
if (vmg.getSpeed() > 0) {
boat.setCurrentSpeed(vmg.getSpeed());
boat.setCurrentSpeed(vmg.getSpeed() * Math.pow(boat.getHealth() / 100, 0.3));
}
}
@@ -677,7 +682,7 @@ public class MockRace extends RaceState {
for (MockBoat boat : this.boats) {
//If the boat is currently racing, count it.
if (boat.getStatus() == BoatStatusEnum.RACING) {
if (boat.getStatus() == BoatStatusEnum.RACING && boat.getHealth()>=1) {
numberOfActiveBoats++;
}
@@ -163,7 +163,7 @@ public class RaceLogic implements RunnableWithFramePeriod, Observer {
//As long as there is at least one boat racing, we still simulate the race.
if (race.getNumberOfActiveBoats() != 0) {
//System.out.println(race.getNumberOfActiveBoats());
//Get the time period of this frame.
long framePeriod = currentTime - previousFrameTime;
@@ -175,12 +175,9 @@ public class RaceLogic implements RunnableWithFramePeriod, Observer {
race.updatePosition(boat, framePeriod, race.getRaceClock().getDurationMilli());
if(race.getColliderRegistry().rayCast(boat)){
//System.out.println("Collision!");
//Add boat to list
collisionBoats.add(boat);
}
//System.out.println(race.getColliderRegistry().rayCast(boat));
}
@@ -64,6 +64,9 @@ public class RaceServer {
//Parse the boat locations.
snapshotMessages.addAll(parseBoatLocations());
//Parse the boat states
snapshotMessages.addAll(parseBoatStates());
//Parse the marks.
snapshotMessages.addAll(parseMarks());
@@ -243,7 +246,29 @@ public class RaceServer {
}
/**
* Generates BoatState messages for every boat in the race
* @return list of BoatState messages
*/
private List<BoatState> parseBoatStates() {
List<BoatState> boatStates = new ArrayList<>();
for(MockBoat boat: race.getBoats()) {
boatStates.add(parseIndividualBoatState(boat));
}
return boatStates;
}
/**
* Creates a BoatState message for the current state of the given boat
* @param boat to generate message for
* @return BoatState message
*/
private BoatState parseIndividualBoatState(MockBoat boat) {
return new BoatState(
boat.getSourceID(),
(int)boat.getHealth()
);
}
/**
* Parses the race status, and returns it.
@@ -21,6 +21,8 @@ import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
/**
* Helper Class for creating a Race XML
@@ -175,10 +177,10 @@ public class RaceXMLCreator {
}
/**
* gets the destance between two marks
* gets the destance between two marks, in nautical miles.
* @param a mark 1
* @param b mark 2
* @return
* @return Distance in nautical miles.
*/
private static double getDistance(XMLMark a, XMLMark b){
GPSCoordinate coorda = new GPSCoordinate(a.getTargetLat(), a.getTargetLng());
@@ -212,12 +214,16 @@ public class RaceXMLCreator {
double averageSpeed = (bestDownWindSpeed + bestUpWindSpeed) / 2;
double raceApproximateTime = getRaceLength(race, averageSpeed);
double scale = milliseconds / raceApproximateTime;
Map<XMLCompoundMark, Boolean> hasBeenScaled = new HashMap<>();
for (XMLCorner cm: race.getCompoundMarkSequence().getCorner()){
int index = cm.getCompoundMarkID() - 1;
XMLCompoundMark mark = race.getCourse().getCompoundMark().get(index);
for (XMLMark m: mark.getMark()){
scalePoint(m, center, scale);
if (!hasBeenScaled.containsKey(mark)) {
for (XMLMark m : mark.getMark()) {
scalePoint(m, center, scale);
}
}
hasBeenScaled.put(mark, true);
}
for (XMLLimit limit: race.getCourseLimit().getLimit()){
scalePoint(limit, center, scale);
@@ -319,8 +325,13 @@ public class RaceXMLCreator {
}
/**
* sets the current race time of the xml
* @param raceXML race xml to alter
* @param racePrestartTime prestart time
* @param racePreparatoryTime preparatory time
* @deprecated this should be used from the RaceXMLCreator not from this function
*/
public static void setRaceXMLAtCurrentTimeToNow(XMLRace raceXML, long racePrestartTime, long racePreparatoryTime){
//The start time is current time + 4 minutes. prestart is 3 minutes, and we add another minute.
long millisecondsToAdd = racePrestartTime + racePreparatoryTime;
@@ -0,0 +1,38 @@
package network.MessageDecoders;
import network.Exceptions.InvalidMessageException;
import network.Messages.AC35Data;
import network.Messages.BoatState;
import java.util.Arrays;
import static network.Utils.ByteConverter.bytesToInt;
/**
* Decoder for {@link BoatState} messages
*/
public class BoatStateDecoder implements MessageDecoder {
/**
* Decoded BoatState message
*/
private BoatState message;
@Override
public AC35Data decode(byte[] encodedMessage) throws InvalidMessageException {
byte[] sourceID = Arrays.copyOfRange(encodedMessage, 0, 4);
byte boatHealth = encodedMessage[4];
// Unpack bytes into BoatState
this.message = new BoatState(
bytesToInt(sourceID),
boatHealth
);
// Return BoatState
return this.message;
}
public BoatState getMessage() {
return message;
}
}
@@ -59,6 +59,7 @@ public class DecoderFactory {
case BOATACTION: return new BoatActionDecoder();
case BOATSTATE: return new BoatStateDecoder();
default: throw new InvalidMessageTypeException("Unrecognised message type: " + type);
}
@@ -18,6 +18,5 @@ public interface MessageDecoder {
* @return The decoded message.
* @throws InvalidMessageException Thrown if the encoded message is invalid in some way, or cannot be decoded.
*/
public AC35Data decode(byte[] encodedMessage) throws InvalidMessageException;
AC35Data decode(byte[] encodedMessage) throws InvalidMessageException;
}
@@ -0,0 +1,32 @@
package network.MessageEncoders;
import network.Exceptions.InvalidMessageException;
import network.Messages.AC35Data;
import network.Messages.BoatState;
import java.nio.ByteBuffer;
import static network.Utils.ByteConverter.intToBytes;
/**
* Encoder for {@link BoatState} message
*/
public class BoatStateEncoder implements MessageEncoder {
@Override
public byte[] encode(AC35Data message) throws InvalidMessageException {
// Downcast message
BoatState boatState = (BoatState)message;
//Serialise message
byte[] sourceID = intToBytes(boatState.getSourceID());
byte boatHealth = (byte)boatState.getBoatHealth();
// Pack bytes into string
ByteBuffer boatStateMessage = ByteBuffer.allocate(5);
boatStateMessage.put(sourceID);
boatStateMessage.put(boatHealth);
// Return byte string
return boatStateMessage.array();
}
}
@@ -59,6 +59,7 @@ public class EncoderFactory {
case BOATACTION: return new BoatActionEncoder();
case BOATSTATE: return new BoatStateEncoder();
default: throw new InvalidMessageTypeException("Unrecognised message type: " + type);
}
@@ -0,0 +1,31 @@
package network.Messages;
import network.Messages.Enums.MessageType;
/**
* Represents the information in a BoatState message according to protocol meeting
*/
public class BoatState extends AC35Data {
/**
* Source ID of boat described in message
*/
private int sourceID;
/**
* Health between 0-100 of boat with above source ID
*/
private int boatHealth;
public BoatState(int sourceID, int boatHealth) {
super(MessageType.BOATSTATE);
this.sourceID = sourceID;
this.boatHealth = boatHealth;
}
public int getSourceID() {
return sourceID;
}
public int getBoatHealth() {
return boatHealth;
}
}
@@ -43,6 +43,8 @@ public enum MessageType {
HOSTED_GAMES_REQUEST(109),
BOATSTATE(103),
NOTAMESSAGE(0);
@@ -56,7 +58,7 @@ public enum MessageType {
* Creates a MessageType enum from a given primitive integer value, cast to a byte.
* @param value Integer, which is cast to byte, to construct from.
*/
private MessageType(int value) {
MessageType(int value) {
this.value = (byte)value;
}
@@ -103,6 +103,11 @@ public class Boat extends Collider {
*/
private boolean isColliding = false;
/**
* Amount of health boat currently has, between 0 and 100
*/
private double health;
/**
* Constructs a boat object with a given sourceID, name, country/team abbreviation, and polars table.
*
@@ -113,6 +118,7 @@ public class Boat extends Collider {
public Boat(int sourceID, String name, String country) {
this.sourceID = sourceID;
this.health = 100;
this.setName(name);
this.setCountry(country);
@@ -419,6 +425,8 @@ public class Boat extends Collider {
@Override
public void onCollisionEnter(Collision e) {
if(e.getBearing().degrees() > 270 || e.getBearing().degrees() < 90) {
// Deplete health
e.getBoat().updateHealth(-5);
// Notify observers of collision
this.setChanged();
notifyObservers(e);
@@ -432,4 +440,22 @@ public class Boat extends Collider {
public void setColliding(boolean colliding) {
isColliding = colliding;
}
public double getHealth() {
return health;
}
public void setHealth(double health) {
this.health = health;
}
/**
* Add a given amount of HP to boat health
* @param delta amount of HP to add
*/
public void updateHealth(double delta) {
health += delta;
if(health < 0) health = 0;
else if(health > 100) health = 100;
}
}
@@ -97,6 +97,7 @@ public class Mark extends Collider{
@Override
public void onCollisionEnter(Collision e) {
e.getBoat().updateHealth(-10);
this.setChanged();
notifyObservers(e);
}
@@ -0,0 +1,30 @@
package visualiser.Commands.VisualiserRaceCommands;
import mock.model.commandFactory.Command;
import network.Messages.BoatState;
import shared.exceptions.BoatNotFoundException;
import visualiser.model.VisualiserBoat;
import visualiser.model.VisualiserRaceState;
/**
* Updates boats on visualiser when their health changes
*/
public class BoatStateCommand implements Command {
private BoatState boatState;
private VisualiserRaceState visualiserRace;
public BoatStateCommand(BoatState boatState, VisualiserRaceState visualiserRace) {
this.boatState = boatState;
this.visualiserRace = visualiserRace;
}
@Override
public void execute() {
try {
VisualiserBoat boat = visualiserRace.getBoat(boatState.getSourceID());
boat.setHealth(boatState.getBoatHealth());
} catch (BoatNotFoundException e) {
// Fail silently
}
}
}
@@ -24,7 +24,6 @@ public class VisualiserRaceCommandFactory {
switch (message.getType()) {
case BOATLOCATION:
//System.out.println("Boat location received");
return new BoatLocationCommand((BoatLocation) message, visualiserRace);
case RACESTATUS: return new RaceStatusCommand((RaceStatus) message, visualiserRace);
@@ -36,7 +35,8 @@ public class VisualiserRaceCommandFactory {
case YACHTEVENTCODE:
return new BoatCollisionCommand((YachtEvent) message, visualiserRace);
case BOATSTATE:
return new BoatStateCommand((BoatState) message, visualiserRace);
default: throw new CommandConstructionException("Could not create VisualiserRaceCommand. Unrecognised or unsupported MessageType: " + message.getType());
@@ -4,6 +4,8 @@ import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import mock.app.Event;
@@ -28,8 +30,12 @@ import java.util.logging.Logger;
*/
public class HostGameController extends Controller {
private @FXML ImageView mapImage;
private @FXML Slider sliderLength;
private @FXML Label lblLength;
private ArrayList<Image> listOfMaps;
private int currentMapIndex = 0;
private int selectedRaceLength; // in minutes
private final int MAX_RACE_LENGTH = 30; // in minutes
private DatagramSocket udpSocket;
private MatchBrowserInterface matchBrowserInterface;
@@ -37,8 +43,33 @@ public class HostGameController extends Controller {
loadMaps();
this.udpSocket = MatchBrowserSingleton.getInstance().getUdpSocket();
this.matchBrowserInterface = MatchBrowserSingleton.getInstance().getMatchBrowserInterface();
setRaceLengthSlider();
}
/**
* Sets up the values and display for a slider object which allows a user
* to select how many minutes long they would like their race to be.
*/
private void setRaceLengthSlider(){
// set the listener to update the label
sliderLength.valueProperty().addListener((ov, old_val, new_val) -> {
selectedRaceLength = new_val.intValue();
if (selectedRaceLength == 1){
lblLength.setText(selectedRaceLength + " minute.");
} else {
lblLength.setText(selectedRaceLength + " minutes.");
}
});
// set values and marks to be displayed
sliderLength.setMin(2);
sliderLength.setMax(MAX_RACE_LENGTH);
sliderLength.setShowTickLabels(true);
sliderLength.setMajorTickUnit(MAX_RACE_LENGTH-1);
sliderLength.setBlockIncrement(1);
sliderLength.getStylesheets().add("/css/slider.css");
}
/**
@@ -64,7 +95,8 @@ public class HostGameController extends Controller {
*/
public void hostGamePressed() {
try {
App.game = new Event(false, currentMapIndex);
App.game = new Event(false, currentMapIndex,
selectedRaceLength*60*1000);
App.gameType = currentMapIndex;
HttpMatchBrowserHost matchBrowserHost = new HttpMatchBrowserHost();
@@ -40,7 +40,7 @@ public class LobbyController extends Controller {
private @FXML TextField addressFld;
private @FXML TextField portFld;
private ObservableList<RaceConnection> connections;
private ObservableList<RaceConnection> allConnections;
private ObservableList<RaceConnection> customConnections;
private AudioClip sound;
@@ -51,12 +51,20 @@ public class LobbyController extends Controller {
public void initialize() {
httpMatchBrowserClient = new HttpMatchBrowserClient();
httpMatchBrowserClient.connections.addListener(new ListChangeListener<RaceConnection>() {
@Override
public void onChanged(Change<? extends RaceConnection> c) {
refreshTable();
}
});
new Thread(httpMatchBrowserClient, "Match Client").start();
// set up the connection table
customConnections = FXCollections.observableArrayList();
allConnections = FXCollections.observableArrayList();
//connections.add(new RaceConnection("localhost", 4942, "Local Game"));
lobbyTable.setItems(httpMatchBrowserClient.connections);
lobbyTable.setItems(allConnections);
gameNameColumn.setCellValueFactory(cellData -> cellData.getValue().gamenameProperty());
hostNameColumn.setCellValueFactory(cellData -> cellData.getValue().hostnameProperty());
statusColumn.setCellValueFactory(cellData -> cellData.getValue().statusProperty());
@@ -82,8 +90,14 @@ public class LobbyController extends Controller {
public void refreshBtnPressed(){
sound = new AudioClip(this.getClass().getResource("/visualiser/sounds/buttonpress.wav").toExternalForm());
sound.play();
refreshTable();
}
private void refreshTable() {
allConnections.clear();
addCustomGames();
addServerGames();
for(RaceConnection connection: connections) {
for(RaceConnection connection: allConnections) {
connection.check();
}
try {
@@ -144,9 +158,9 @@ public class LobbyController extends Controller {
try {
int port = Integer.parseInt(portString);
customConnections.add(new RaceConnection(hostName, port, "Boat Game"));
connections.addAll(customConnections);
addressFld.clear();
portFld.clear();
refreshTable();
} catch (NumberFormatException e) {
System.err.println("Port number entered is not a number");
}
@@ -173,17 +187,14 @@ public class LobbyController extends Controller {
* Adds the games received from the server
*/
private void addServerGames() {
httpMatchBrowserClient.connections.addAll(customConnections);
httpMatchBrowserClient.connections.addListener(new ListChangeListener<RaceConnection>() {
@Override
public void onChanged(Change<? extends RaceConnection> c) {
refreshBtnPressed();
}
});
allConnections.addAll(httpMatchBrowserClient.connections);
/*
for (HostGame game : matchBrowserLobbyInterface.getGames()) {
connections.add(new RaceConnection(game.getIp(), 4942, "Boat Game"));
}*/
}
private void addCustomGames() {
allConnections.addAll(customConnections);
}
}
@@ -0,0 +1,123 @@
package visualiser.Controllers;
import com.interactivemesh.jfx.importer.stl.StlMeshImporter;
import javafx.animation.AnimationTimer;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.geometry.Point3D;
import javafx.scene.AmbientLight;
import javafx.scene.control.Label;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Cylinder;
import javafx.scene.shape.MeshView;
import javafx.scene.shape.Shape3D;
import javafx.scene.transform.Rotate;
import shared.model.Bearing;
import shared.model.CompoundMark;
import shared.model.GPSCoordinate;
import visualiser.layout.Annotation3D;
import visualiser.layout.Assets3D;
import visualiser.layout.Subject3D;
import visualiser.layout.View3D;
import visualiser.model.VisualiserBoat;
import java.net.URL;
import java.util.Observable;
import java.util.Observer;
public class NextMarkController {
private @FXML StackPane arrowStackPane2d;
private @FXML StackPane arrowStackPane3d;
private @FXML Pane pane2d;
private @FXML Pane pane3d;
private VisualiserBoat boat;
public void initialiseArrowView(VisualiserBoat boat) {
this.boat = boat;
pane2d.setVisible(true);
pane3d.setVisible(false);
initialise2dArrowView();
initialise3dArrowView();
}
private void initialise2dArrowView() {
AnimationTimer arrow2d = new AnimationTimer() {
@Override
public void handle(long now) {
if (boat.getCurrentLeg().getEndCompoundMark() != null) {
CompoundMark target = boat.getCurrentLeg().getEndCompoundMark();
Bearing headingToMark = GPSCoordinate.calculateBearing(boat.getPosition(), target.getAverageGPSCoordinate());
arrowStackPane2d.setRotate(headingToMark.degrees());
} else {
stop();
}
}
};
arrow2d.start();
}
private void initialise3dArrowView() {
ObservableList<Subject3D> viewSubjects = FXCollections.observableArrayList();
String arrowPath = "assets/mark_arrow.x3d";
Shape3D arrow = Assets3D.loadX3d(arrowPath);
arrow.setScaleX(25);
arrow.setScaleY(25);
arrow.setScaleZ(100);
arrow.setRotationAxis(new Point3D(1,0,0));
arrowStackPane3d.getChildren().add(arrow);
AnimationTimer arrow3d = new AnimationTimer() {
@Override
public void handle(long now) {
if (boat.getCurrentLeg().getEndCompoundMark() != null) {
arrow.getTransforms().clear();
double zRotation = calculateZRotate();
arrow.setRotate(calculateXRotate(zRotation));
arrow.getTransforms().add(new Rotate(zRotation, new Point3D(0, 0, 1)));
} else {
stop();
}
}
};
arrow3d.start();
}
public void show2d() {
pane3d.setVisible(false);
pane2d.setVisible(true);
}
public void show3d() {
pane2d.setVisible(false);
pane3d.setVisible(true);
}
private double calculateZRotate() {
CompoundMark target = boat.getCurrentLeg().getEndCompoundMark();
Bearing headingToMark = GPSCoordinate.calculateBearing(boat.getPosition(), target.getAverageGPSCoordinate());
return -headingToMark.degrees() + boat.getBearing().degrees() + 180;
}
private double calculateXRotate(double zRotation) {
// if (zRotation > 360) {
// zRotation -=360;
// } else if (zRotation < 0) {
// zRotation += 360;
// }
//
// if (zRotation > 180) {
// zRotation = 360 - zRotation;
// }
return 70;
//return 90 - 20 * Math.cos(Math.toRadians(zRotation));
}
}
@@ -15,19 +15,25 @@ import javafx.scene.AmbientLight;
import javafx.scene.PointLight;
import javafx.scene.chart.LineChart;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.media.AudioClip;
import javafx.scene.paint.Color;
import javafx.scene.paint.Material;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Cylinder;
import javafx.scene.paint.Stop;
import javafx.scene.shape.MeshView;
import javafx.scene.shape.Shape3D;
import javafx.scene.transform.Translate;
import javafx.util.Callback;
import network.Messages.Enums.BoatStatusEnum;
import network.Messages.Enums.RaceStatusEnum;
import shared.dataInput.RaceDataSource;
import shared.enums.RoundingType;
@@ -43,6 +49,7 @@ import visualiser.model.*;
import visualiser.utils.GPSConverter;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.*;
import java.util.logging.Level;
@@ -72,6 +79,8 @@ public class RaceViewController extends Controller {
private ResizableRaceCanvas raceCanvas;
private boolean mapToggle = true;
private GPSConverter gpsConverter;
private ArrayList<HealthEffect> healthEffectList = new ArrayList<>();
/**
* Arrow pointing to next mark in third person
@@ -84,10 +93,12 @@ public class RaceViewController extends Controller {
// note: it says it's not used but it is! do not remove :)
private @FXML ArrowController arrowController;
private @FXML NextMarkController nextMarkController;
private @FXML GridPane canvasBase;
private @FXML GridPane canvasBase1;
private @FXML SplitPane racePane;
private @FXML StackPane arrowPane;
private @FXML Pane nextMarkPane;
private @FXML Label timer;
private @FXML Label FPS;
private @FXML Label timeZone;
@@ -97,12 +108,17 @@ public class RaceViewController extends Controller {
private @FXML TableColumn<VisualiserBoat, String> boatTeamColumn;
private @FXML TableColumn<VisualiserBoat, Leg> boatMarkColumn;
private @FXML TableColumn<VisualiserBoat, Number> boatSpeedColumn;
private @FXML TableColumn<VisualiserBoat, Number> boatHealthColumn;
private @FXML LineChart<Number, Number> sparklineChart;
private @FXML Label tutorialText;
private @FXML AnchorPane infoWrapper;
private @FXML AnchorPane lineChartWrapper;
private @FXML StackPane speedPane;
private @FXML AnchorPane raceAnchorPane;
private @FXML GridPane playerHealthContainer;
private @FXML ImageView imageView;
private @FXML AnchorPane deathTransPane;
private @FXML StackPane deathPane;
/**
* Displays a specified race.
@@ -117,11 +133,13 @@ public class RaceViewController extends Controller {
this.controllerClient = controllerClient;
this.isHost = isHost;
keyFactory.load();
deathPane.setDisable(false);
deathPane.setVisible(false);
tutorialCheck();
initKeypressHandler();
initialiseRaceVisuals();
initialiseRaceCanvas();
healthLoop();
}
/**
@@ -133,7 +151,6 @@ public class RaceViewController extends Controller {
isTutorial = true;
tutorialText.setVisible(true);
tutorialStates = new ArrayList<>(Arrays.asList(TutorialState.values()));
currentState = tutorialStates.get(0);
tutorialStates.remove(0);
searchMapForKey("Upwind");
@@ -290,33 +307,47 @@ 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(this.visualiserRace);
initialiseHealthPane();
initialiseRaceClock();
initialiseSpeedometer();
raceTimer(); // start the timer
nextMarkPane.toFront();
speedometerLoop();
new Sparkline(this.raceState, this.sparklineChart);
timeZone.setText(this.raceState.getRaceClock().getTimeZone());
arrowController.setWindProperty(this.raceState.windProperty());
}
private void initialiseHealthPane() {
InputStream tomato = this.getClass().getClassLoader().getResourceAsStream("visualiser/images/tomato.png");
HealthSlider healthSlider = new HealthSlider(new Image(tomato));
playerHealthContainer.add(healthSlider, 0, 0);
try {
VisualiserBoat player = raceState.getBoat(raceState.getPlayerBoatID());
player.healthProperty().addListener((o, prev, curr) -> {
healthSlider.setCrop((double)curr/100.0);
});
} catch (BoatNotFoundException e) {
e.printStackTrace();
}
}
private void initialiseView3D(VisualiserRaceEvent race) {
viewSubjects = FXCollections.observableArrayList();
try {
nextMarkController.initialiseArrowView(race.getVisualiserRaceState().getBoat(race.getVisualiserRaceState().getPlayerBoatID()));
} catch (BoatNotFoundException e) {
e.printStackTrace();
}
AmbientLight ambientLight = new AmbientLight(Color.web("#CCCCFF"));
ambientLight.setTranslateX(250);
ambientLight.setTranslateZ(210);
@@ -406,11 +437,29 @@ public class RaceViewController extends Controller {
viewSubjects.add(markModel);
}
for (VisualiserBoat boat: race.getVisualiserRaceState().getBoats()) {
// Position and add each boat to view
for(VisualiserBoat boat: race.getVisualiserRaceState().getBoats()) {
Shape3D mesh = Assets3D.getBoat();
PhongMaterial boatColorMat = new PhongMaterial(boat.getColor());
//mesh.setMaterial(boatColorMat);
Subject3D boatModel = new Subject3D(mesh, boat.getSourceID());
// Configure visualiser for client's boat
if (boat.isClientBoat()) {
// Add player boat highlight
Shockwave boatHighlight = new Shockwave(10);
boatHighlight.getMesh().setMaterial(new PhongMaterial(new Color(1, 1, 0, 0.1)));
viewSubjects.add(boatHighlight);
// Track player boat with camera
viewSubjects.add(boatModel);
Platform.runLater(() -> {
view3D.trackSubject(boatModel);
view3D.setThirdPerson();
});
// Track player boat with highlight
AnimationTimer highlightTrack = new AnimationTimer() {
@Override
public void handle(long now) {
@@ -419,28 +468,28 @@ public class RaceViewController extends Controller {
}
};
highlightTrack.start();
// Highlight next mark only for player boat
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);
}
boat.legProperty().addListener((o, prev, curr) -> Platform.runLater(() -> swapColours(curr)));
} else {
viewSubjects.add(boatModel);
}
}
// Position and add each boat to view
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());
// }
Shape3D mesh = Assets3D.getBoat();
PhongMaterial boatColorMat = new PhongMaterial(boat.getColor());
//mesh.setMaterial(boatColorMat);
Subject3D boatModel = new Subject3D(mesh, boat.getSourceID());
viewSubjects.add(boatModel);
//Create health effect
HealthEffect healthEffect = new HealthEffect(boat.getSourceID(), System.currentTimeMillis());
viewSubjects.add(healthEffect);
healthEffectList.add(healthEffect);
//add sail
Sails3D sails3D = new Sails3D();
Subject3D sailsSubject = new Subject3D(sails3D, 0);
sails3D.setMouseTransparent(true);
sails3D.setMaterial(boatColorMat);
sailsSubject.setXRot(0d);
sailsSubject.setHeading(visualiserRace.getVisualiserRaceState().getWindDirection().degrees());
@@ -517,6 +566,14 @@ public class RaceViewController extends Controller {
boatModel.setHeading(boat.getBearing().degrees());
boatModel.setX(gpsConverter.convertGPS(boat.getPosition()).getX());
boatModel.setZ(gpsConverter.convertGPS(boat.getPosition()).getY());
boatModel.getMesh().toFront();
//Fire follows boat
healthEffect.setHeading(boat.getBearing().degrees());
healthEffect.setX(gpsConverter.convertGPS(boat.getPosition()).getX());
healthEffect.setZ(gpsConverter.convertGPS(boat.getPosition()).getY());
healthEffect.setY(0);
}
};
trackBoat.start();
@@ -543,7 +600,6 @@ public class RaceViewController extends Controller {
}*/
Subject3D shockwave = new Shockwave(10);
viewSubjects.add(shockwave);
//boat.legProperty().addListener((o, prev, curr) -> Platform.runLater(() -> swapColours(curr)));
@@ -632,37 +688,32 @@ public class RaceViewController extends Controller {
}
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();
if (target != null) {
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();
nextMarkController.show3d();
// 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();
}
nextMarkController.show2d();
}
/**
@@ -789,13 +840,17 @@ public class RaceViewController extends Controller {
// set table data
boatInfoTable.setItems(sortedBoats);
boatTeamColumn.setCellValueFactory(
cellData -> cellData.getValue().nameProperty());
cellData -> cellData.getValue().nameProperty()
);
boatSpeedColumn.setCellValueFactory(
cellData -> cellData.getValue().currentSpeedProperty());
cellData -> cellData.getValue().currentSpeedProperty()
);
boatMarkColumn.setCellValueFactory(
cellData -> cellData.getValue().legProperty());
boatPlacingColumn.setCellValueFactory(
cellData -> cellData.getValue().placingProperty());
cellData -> cellData.getValue().legProperty()
);
boatHealthColumn.setCellValueFactory(
cellData -> cellData.getValue().healthProperty()
);
//Kind of ugly, but allows for formatting an observed speed.
boatSpeedColumn.setCellFactory(
@@ -927,6 +982,64 @@ public class RaceViewController extends Controller {
}.start();
}
/**
* Animation timer for health
*/
private void healthLoop(){
new AnimationTimer(){
@Override
public void handle(long arg0){
if (raceState.getRaceStatusEnum() == RaceStatusEnum.FINISHED) {
stop(); // stop the timer
} else {
try {
//Check if boat is dead
if(raceState.getBoat(raceState.getPlayerBoatID()).getHealth()<=0){
if(!deathPane.isDisable()) {
deathPane.setVisible(true);
}
}
} catch (BoatNotFoundException e) {
e.printStackTrace();
}
for(VisualiserBoat boat : raceState.getBoats()){
for (HealthEffect fp : healthEffectList){
if(fp.getSourceID()==boat.getSourceID()){
if(boat.getHealth()<=0){
//Boat is dead. Don't check it anymore for hp
fp.displayDeath(fp.getSourceID()==raceState.getPlayerBoatID());
fp.setSourceID(0);
try {
raceState.getBoat(boat.getSourceID()).setStatus(BoatStatusEnum.DNF);
raceState.updateBoatPositions(raceState.getBoats());
} catch (BoatNotFoundException e) {
e.printStackTrace();
}
}
else
//Speed up tick when <=10 hp
if(boat.getHealth()<=10){
fp.flash(System.currentTimeMillis(), 300, boat.getSourceID()==raceState.getPlayerBoatID());
}
else
//Visual indication of low hp
if(boat.getHealth()<=20) {
//fp.setVisible(true);
fp.flash(System.currentTimeMillis(), 500, boat.getSourceID()==raceState.getPlayerBoatID());
} else {
fp.setVisible(false);
}
break;
}
}
}
}
}
}.start();
}
/**
* toggles if the info table is shown
@@ -1082,4 +1195,12 @@ public class RaceViewController extends Controller {
mapToggle = !mapToggle;
}
/**
* FXML method for death button
*/
public void deathOKPressed(){
deathPane.setDisable(true);
deathPane.setVisible(false);
}
}
@@ -104,7 +104,7 @@ public class Assets3D {
windArrow = new Annotation3D(loadX3d(arrowPath));
}
private static Shape3D loadX3d(String path){
public static Shape3D loadX3d(String path){
X3dModelImporter x3dModelImporter = new X3dModelImporter();
URL asset = Assets3D.class.getClassLoader().getResource(path);
x3dModelImporter.read(asset);
@@ -0,0 +1,111 @@
package visualiser.layout;
import javafx.geometry.Point3D;
import javafx.scene.image.Image;
import javafx.scene.media.AudioClip;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.*;
import javafx.scene.transform.Rotate;
/**
* Created by zwu18 on 24/09/17.
*/
public class HealthEffect extends Subject3D {
private int sourceID;
private long currentTime;
private AudioClip warningSound = new AudioClip(this.getClass().getResource("/visualiser/sounds/warning.mp3").toExternalForm());
private AudioClip deadSound = new AudioClip(this.getClass().getResource("/visualiser/sounds/dead1.wav").toExternalForm());
public HealthEffect(int sourceID, long currentTime){
super(createEffect(), 0);
this.sourceID = sourceID;
this.currentTime = currentTime;
}
/**
* Initialise the mesh view with image
* @return Mesh view
*/
private static Shape3D createEffect(){
Image image = new Image(HealthEffect.class.getClassLoader().getResourceAsStream("images/warning.png"));
Plane3D plane = new Plane3D(20, 20, 10, 10);
PhongMaterial material = new PhongMaterial();
material.setDiffuseColor(Color.web("#FFFFFF"));
material.setSpecularColor(Color.web("#000000"));
material.setDiffuseMap(image);
MeshView imageSurface = new MeshView(plane);
imageSurface.setMaterial(material);
imageSurface.setMouseTransparent(true);
//imageSurface.toFront(); this.flashInterval = flashInterval;
return imageSurface;
}
public void rotateView(Double angle, Double pivotX, Double pivotY, Double pivotZ, Point3D axis){
Rotate rotate = new Rotate(angle, axis);
rotate.setPivotX(pivotX);
rotate.setPivotY(pivotY);
rotate.setPivotZ(pivotZ);
this.getMesh().getTransforms().add(rotate);
}
public void setVisible(boolean bool){
this.getMesh().setVisible(bool);
}
public int getSourceID(){
return sourceID;
}
public void setSourceID(int id){
this.sourceID = id;
}
/**
* Display visual indication when boat dies
* @param player boolean if player is current user or not
*/
public void displayDeath(boolean player){
Image image = new Image(HealthEffect.class.getClassLoader().getResourceAsStream("images/warning2.png"));
PhongMaterial material = (PhongMaterial) this.getMesh().getMaterial();
material.setDiffuseColor(Color.web("#FFFFFF"));
material.setSpecularColor(Color.web("#000000"));
material.setDiffuseMap(image);
this.getMesh().setMaterial(material);
if(player) {
deadSound.play();
}
}
/**
* Flash the mesh view at a certain interval
* @param checkTime The current time of flash
* @param flashInterval Desired flash interval
* @param playerBoat Whether or not this effect is for the player's boat.
*/
public void flash(long checkTime, long flashInterval, boolean playerBoat){
if(checkTime >= (currentTime+flashInterval)){
this.currentTime = checkTime;
if(this.getMesh().isVisible()){
this.setVisible(false);
} else {
if(playerBoat) {
warningSound.setVolume(0.1);
warningSound.play();
}
this.setVisible(true);
}
}
}
}
@@ -0,0 +1,66 @@
package visualiser.layout;
import javafx.scene.image.Image;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.*;
import javafx.scene.shape.Rectangle;
import java.awt.image.BufferedImage;
/**
* Created by connortaylorbrown on 21/09/17.
*/
public class HealthSlider extends Pane {
/**
* Image used to fill health slider
*/
private Image fillImage;
/**
* Size of background for image configuration
*/
private BackgroundSize backgroundSize;
/**
* Percentage of image cropped out from top
*/
private double crop;
public HealthSlider(Image fillImage) {
this.fillImage = fillImage;
this.crop = 1;
this.backgroundSize = new BackgroundSize(
100,
100,
true,
true,
true,
false);
drawSlider();
}
public void setCrop(double crop) {
this.crop = crop;
drawSlider();
}
private void drawSlider() {
int top = Math.max(0,(int)(fillImage.getHeight() - crop * fillImage.getHeight()));
WritableImage croppedImage = new WritableImage(
fillImage.getPixelReader(),
0,
top,
(int)fillImage.getWidth(),
(int)fillImage.getHeight() - top
);
BackgroundImage backgroundImage = new BackgroundImage(
croppedImage,
BackgroundRepeat.NO_REPEAT,
BackgroundRepeat.NO_REPEAT,
BackgroundPosition.CENTER,
backgroundSize
);
this.setBackground(new Background(backgroundImage));
}
}
@@ -81,7 +81,7 @@ public class View3D extends Pane {
/**
* Distance to stop zoom
*/
private final double ZOOM_IN_LIMIT = 30;
private final double ZOOM_IN_LIMIT = 3;
private final double ZOOM_OUT_LIMIT = 700;
private final double MAX_ZOOM_LIMIT = 1500;
private final double MAX_PITCH = 60; // birds eye view
@@ -1,9 +1,6 @@
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.beans.property.*;
import javafx.scene.paint.Color;
import network.Messages.Enums.BoatStatusEnum;
import shared.model.*;
@@ -65,6 +62,7 @@ public class VisualiserBoat extends Boat {
private ObjectProperty<GPSCoordinate> positionProperty;
private ObjectProperty<Bearing> bearingProperty;
private BooleanProperty hasCollided;
private DoubleProperty healthProperty;
/**
@@ -78,6 +76,7 @@ public class VisualiserBoat extends Boat {
this.color = color;
this.hasCollided = new SimpleBooleanProperty(false);
this.healthProperty = new SimpleDoubleProperty(100);
}
@@ -284,4 +283,18 @@ public class VisualiserBoat extends Boat {
public void setHasCollided(boolean hasCollided) {
this.hasCollided.set(hasCollided);
}
public DoubleProperty healthProperty() {
return healthProperty;
}
@Override
public double getHealth() {
return healthProperty.get();
}
@Override
public void setHealth(double healthProperty) {
this.healthProperty.set((int)healthProperty);
}
}
@@ -248,7 +248,6 @@ public class VisualiserRaceState extends RaceState {
if ((boat.getStatus() == BoatStatusEnum.DNF) || (boat.getStatus() == BoatStatusEnum.PRESTART) || (boat.getCurrentLeg().getLegNumber() < 0)) {
boat.setPlacing("-");
} else {
@@ -297,6 +297,7 @@ public class ServerConnection implements RunnableWithFramePeriod {
this.messageRouter.addRoute(MessageType.MARKROUNDING, incomingMessages);
this.messageRouter.addRoute(MessageType.XMLMESSAGE, incomingMessages);
this.messageRouter.addRoute(MessageType.ASSIGN_PLAYER_BOAT, incomingMessages);
this.messageRouter.addRoute(MessageType.BOATSTATE, incomingMessages);
this.messageRouter.removeDefaultRoute(); //We no longer want to keep un-routed messages.