diff --git a/client/src/main/java/nl/andrewl/aos2_client/Camera.java b/client/src/main/java/nl/andrewl/aos2_client/Camera.java
index fb936d0..80464c8 100644
--- a/client/src/main/java/nl/andrewl/aos2_client/Camera.java
+++ b/client/src/main/java/nl/andrewl/aos2_client/Camera.java
@@ -1,6 +1,7 @@
package nl.andrewl.aos2_client;
import nl.andrewl.aos_core.MathUtils;
+import nl.andrewl.aos_core.net.udp.ClientOrientationState;
import org.joml.Matrix4f;
import org.joml.Vector2f;
import org.joml.Vector3f;
@@ -19,6 +20,8 @@ public class Camera implements GLFWCursorPosCallbackI {
public static final Vector3f FORWARD = new Vector3f(0, 0, -1);
public static final Vector3f BACKWARD = new Vector3f(0, 0, 1);
+ private final Client client;
+
/**
* The x, y, and z position of the camera in the world.
*/
@@ -44,7 +47,8 @@ public class Camera implements GLFWCursorPosCallbackI {
private float lastMouseCursorY;
private float mouseCursorSensitivity = 0.005f;
- public Camera() {
+ public Camera(Client client) {
+ this.client = client;
this.position = new Vector3f();
this.orientation = new Vector2f(0, (float) (Math.PI / 2));
this.viewTransform = new Matrix4f();
@@ -63,9 +67,11 @@ public class Camera implements GLFWCursorPosCallbackI {
}
public void setPosition(float x, float y, float z) {
- position.set(x, y, z);
- updateViewTransform();
- System.out.printf("Position: x=%.2f, y=%.2f, z=%.2f%n", position.x, position.y, position.z);
+ if (position.x != x || position.y != y || position.z != z) {
+ position.set(x, y, z);
+ updateViewTransform();
+ System.out.printf("Position: x=%.2f, y=%.2f, z=%.2f%n", position.x, position.y, position.z);
+ }
}
public void setOrientation(float x, float y) {
@@ -100,17 +106,19 @@ public class Camera implements GLFWCursorPosCallbackI {
lastMouseCursorX = x;
lastMouseCursorY = y;
setOrientation(orientation.x - dx * mouseCursorSensitivity, orientation.y - dy * mouseCursorSensitivity);
+ client.getCommunicationHandler().sendDatagramPacket(new ClientOrientationState(client.getClientId(), orientation.x, orientation.y));
// System.out.printf("rX=%.0f deg about the Y axis, rY=%.0f deg about the X axis%n", Math.toDegrees(orientation.x), Math.toDegrees(orientation.y));
var vv = getViewVector();
// System.out.printf("View vector: [%.2f, %.2f, %.2f]%n", vv.x, vv.y, vv.z);
}
public Vector3f getViewVector() {
+ float y = (float) (orientation.y + Math.PI / 2);
return new Vector3f(
- (float) -Math.sin(orientation.x),
- (float) -Math.cos(orientation.y),
- (float) Math.cos(orientation.x)
- );
+ (float) (Math.sin(orientation.x) * Math.cos(y)),
+ (float) -Math.sin(y),
+ (float) (Math.cos(orientation.x) * Math.cos(y))
+ ).normalize();
}
public void move(Vector3f relativeMotion) {
@@ -120,6 +128,6 @@ public class Camera implements GLFWCursorPosCallbackI {
moveTransform.transformDirection(actualMotion);
position.add(actualMotion);
updateViewTransform();
- System.out.printf("Position: x=%.2f, y=%.2f, z=%.2f%n", position.x, position.y, position.z);
+// System.out.printf("Position: x=%.2f, y=%.2f, z=%.2f%n", position.x, position.y, position.z);
}
}
diff --git a/client/src/main/java/nl/andrewl/aos2_client/Client.java b/client/src/main/java/nl/andrewl/aos2_client/Client.java
index 715a169..ec0d2c9 100644
--- a/client/src/main/java/nl/andrewl/aos2_client/Client.java
+++ b/client/src/main/java/nl/andrewl/aos2_client/Client.java
@@ -4,6 +4,7 @@ import nl.andrewl.aos2_client.render.ChunkMesh;
import nl.andrewl.aos2_client.render.ChunkRenderer;
import nl.andrewl.aos2_client.render.WindowUtils;
import nl.andrewl.aos_core.model.World;
+import nl.andrewl.aos_core.net.udp.ClientInputState;
import java.io.IOException;
import java.net.InetAddress;
@@ -26,8 +27,10 @@ public class Client implements Runnable {
private String username;
private CommunicationHandler communicationHandler;
private ChunkRenderer chunkRenderer;
+ private int clientId;
private World world;
+ private Camera cam;
public Client(InetAddress serverAddress, int serverPort, String username) {
this.serverAddress = serverAddress;
@@ -35,6 +38,7 @@ public class Client implements Runnable {
this.username = username;
this.communicationHandler = new CommunicationHandler(this);
this.world = new World();
+ this.cam = new Camera(this);
}
@Override
@@ -44,7 +48,7 @@ public class Client implements Runnable {
chunkRenderer = new ChunkRenderer(windowInfo.width(), windowInfo.height());
try {
- communicationHandler.establishConnection(serverAddress, serverPort, username);
+ this.clientId = communicationHandler.establishConnection(serverAddress, serverPort, username);
System.out.println("Established connection to the server.");
} catch (IOException e) {
e.printStackTrace();
@@ -61,11 +65,9 @@ public class Client implements Runnable {
chunkRenderer.addChunkMesh(new ChunkMesh(chunk));
}
- Camera cam = new Camera();
- cam.setOrientationDegrees(90, 90);
- cam.setPosition(0, 48, 0);
glfwSetCursorPosCallback(windowHandle, cam);
+ ClientInputState lastInputState = null;
while (!glfwWindowShouldClose(windowHandle)) {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
@@ -75,12 +77,20 @@ public class Client implements Runnable {
glfwSwapBuffers(windowHandle);
glfwPollEvents();
- if (glfwGetKey(windowHandle, GLFW_KEY_W) == GLFW_PRESS) cam.move(Camera.FORWARD);
- if (glfwGetKey(windowHandle, GLFW_KEY_S) == GLFW_PRESS) cam.move(Camera.BACKWARD);
- if (glfwGetKey(windowHandle, GLFW_KEY_A) == GLFW_PRESS) cam.move(Camera.LEFT);
- if (glfwGetKey(windowHandle, GLFW_KEY_D) == GLFW_PRESS) cam.move(Camera.RIGHT);
- if (glfwGetKey(windowHandle, GLFW_KEY_SPACE) == GLFW_PRESS) cam.move(Camera.UP);
- if (glfwGetKey(windowHandle, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS) cam.move(Camera.DOWN);
+ ClientInputState inputState = new ClientInputState(
+ clientId,
+ glfwGetKey(windowHandle, GLFW_KEY_W) == GLFW_PRESS,
+ glfwGetKey(windowHandle, GLFW_KEY_S) == GLFW_PRESS,
+ glfwGetKey(windowHandle, GLFW_KEY_A) == GLFW_PRESS,
+ glfwGetKey(windowHandle, GLFW_KEY_D) == GLFW_PRESS,
+ glfwGetKey(windowHandle, GLFW_KEY_SPACE) == GLFW_PRESS,
+ glfwGetKey(windowHandle, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS,
+ false
+ );
+ if (!inputState.equals(lastInputState)) {
+ communicationHandler.sendDatagramPacket(inputState);
+ lastInputState = inputState;
+ }
}
communicationHandler.shutdown();
@@ -89,10 +99,22 @@ public class Client implements Runnable {
WindowUtils.clearUI(windowHandle);
}
+ public int getClientId() {
+ return clientId;
+ }
+
public World getWorld() {
return world;
}
+ public Camera getCam() {
+ return cam;
+ }
+
+ public CommunicationHandler getCommunicationHandler() {
+ return communicationHandler;
+ }
+
public ChunkRenderer getChunkRenderer() {
return chunkRenderer;
}
diff --git a/client/src/main/java/nl/andrewl/aos2_client/CommunicationHandler.java b/client/src/main/java/nl/andrewl/aos2_client/CommunicationHandler.java
index 8bab550..1113646 100644
--- a/client/src/main/java/nl/andrewl/aos2_client/CommunicationHandler.java
+++ b/client/src/main/java/nl/andrewl/aos2_client/CommunicationHandler.java
@@ -4,9 +4,12 @@ import nl.andrewl.aos_core.Net;
import nl.andrewl.aos_core.model.Chunk;
import nl.andrewl.aos_core.net.*;
import nl.andrewl.aos_core.net.udp.DatagramInit;
+import nl.andrewl.aos_core.net.udp.PlayerUpdateMessage;
import nl.andrewl.record_net.Message;
import nl.andrewl.record_net.util.ExtendedDataInputStream;
import nl.andrewl.record_net.util.ExtendedDataOutputStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.DatagramPacket;
@@ -20,6 +23,8 @@ import java.net.Socket;
* methods for sending messages and processing those we receive.
*/
public class CommunicationHandler {
+ private static final Logger log = LoggerFactory.getLogger(CommunicationHandler.class);
+
private final Client client;
private Socket socket;
private DatagramSocket datagramSocket;
@@ -29,9 +34,9 @@ public class CommunicationHandler {
public CommunicationHandler(Client client) {
this.client = client;
}
-
+
public int establishConnection(InetAddress address, int port, String username) throws IOException {
- System.out.printf("Connecting to server at %s, port %d, with username \"%s\"...%n", address, port, username);
+ log.debug("Connecting to server at {}, port {}, with username \"{}\"...", address, port, username);
if (socket != null && !socket.isClosed()) {
socket.close();
}
@@ -109,11 +114,10 @@ public class CommunicationHandler {
if (!connectionEstablished) {
throw new IOException("Could not establish a datagram connection to the server after " + attempts + " attempts.");
}
- System.out.println("Established datagram communication with the server.");
+ log.debug("Established datagram communication with the server.");
}
private void handleMessage(Message msg) {
- System.out.println("Received message: " + msg);
if (msg instanceof ChunkDataMessage chunkDataMessage) {
Chunk chunk = chunkDataMessage.toChunk();
client.getWorld().addChunk(chunk);
@@ -121,6 +125,11 @@ public class CommunicationHandler {
}
private void handleUdpMessage(Message msg, DatagramPacket packet) {
- System.out.println("Received udp message: " + msg);
+ if (msg instanceof PlayerUpdateMessage playerUpdate) {
+// log.debug("Received player update: {}", playerUpdate);
+ if (playerUpdate.clientId() == client.getClientId()) {
+ client.getCam().setPosition(playerUpdate.px(), playerUpdate.py() + 1.8f, playerUpdate.pz());
+ }
+ }
}
}
diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/ChunkRenderer.java b/client/src/main/java/nl/andrewl/aos2_client/render/ChunkRenderer.java
index 89e90c5..8039a25 100644
--- a/client/src/main/java/nl/andrewl/aos2_client/render/ChunkRenderer.java
+++ b/client/src/main/java/nl/andrewl/aos2_client/render/ChunkRenderer.java
@@ -18,7 +18,6 @@ public class ChunkRenderer {
private final ShaderProgram shaderProgram;
private final int projectionTransformUniform;
private final int viewTransformUniform;
- private final int normalTransformUniform;
private final int chunkPositionUniform;
private final int chunkSizeUniform;
@@ -35,7 +34,6 @@ public class ChunkRenderer {
shaderProgram.use();
this.projectionTransformUniform = shaderProgram.getUniform("projectionTransform");
this.viewTransformUniform = shaderProgram.getUniform("viewTransform");
- this.normalTransformUniform = shaderProgram.getUniform("normalTransform");
this.chunkPositionUniform = shaderProgram.getUniform("chunkPosition");
this.chunkSizeUniform = shaderProgram.getUniform("chunkSize");
diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/WindowUtils.java b/client/src/main/java/nl/andrewl/aos2_client/render/WindowUtils.java
index c5d0bbb..49d3db5 100644
--- a/client/src/main/java/nl/andrewl/aos2_client/render/WindowUtils.java
+++ b/client/src/main/java/nl/andrewl/aos2_client/render/WindowUtils.java
@@ -17,7 +17,10 @@ public class WindowUtils {
var vidMode = glfwGetVideoMode(glfwGetPrimaryMonitor());
if (vidMode == null) throw new IllegalStateException("Could not get information about the primary monitory.");
- long windowHandle = glfwCreateWindow(vidMode.width(), vidMode.height(), "Ace of Shades 2", glfwGetPrimaryMonitor(), 0);
+ int width = vidMode.width();
+ int height = vidMode.height();
+ width = 800; height = 600;
+ long windowHandle = glfwCreateWindow(width, height, "Ace of Shades 2", 0, 0);
if (windowHandle == 0) throw new RuntimeException("Failed to create GLFW window.");
glfwSetKeyCallback(windowHandle, (window, key, scancode, action, mods) -> {
@@ -43,7 +46,7 @@ public class WindowUtils {
glEnable(GL_DEPTH_TEST);
glCullFace(GL_BACK);
- return new WindowInfo(windowHandle, vidMode.width(), vidMode.height());
+ return new WindowInfo(windowHandle, width, height);
}
public static void clearUI(long windowHandle) {
diff --git a/core/pom.xml b/core/pom.xml
index 3425060..a45f2f6 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -33,7 +33,7 @@
com.github.andrewlalis
record-net
- v1.2.1
+ v1.3.4
@@ -41,6 +41,19 @@
zero-allocation-hashing
0.15
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.36
+
+
+
+ org.apache.logging.log4j
+ log4j-slf4j-impl
+ 2.18.0
+
+
diff --git a/core/src/main/java/nl/andrewl/aos_core/Net.java b/core/src/main/java/nl/andrewl/aos_core/Net.java
index 71a3d22..90a27c8 100644
--- a/core/src/main/java/nl/andrewl/aos_core/Net.java
+++ b/core/src/main/java/nl/andrewl/aos_core/Net.java
@@ -1,7 +1,7 @@
package nl.andrewl.aos_core;
import nl.andrewl.aos_core.net.*;
-import nl.andrewl.aos_core.net.udp.DatagramInit;
+import nl.andrewl.aos_core.net.udp.*;
import nl.andrewl.record_net.Message;
import nl.andrewl.record_net.Serializer;
import nl.andrewl.record_net.util.ExtendedDataInputStream;
@@ -26,6 +26,10 @@ public final class Net {
serializer.registerType(4, DatagramInit.class);
serializer.registerType(5, ChunkHashMessage.class);
serializer.registerType(6, ChunkDataMessage.class);
+ serializer.registerType(7, ChunkUpdateMessage.class);
+ serializer.registerType(8, ClientInputState.class);
+ serializer.registerType(9, ClientOrientationState.class);
+ serializer.registerType(10, PlayerUpdateMessage.class);
}
public static ExtendedDataInputStream getInputStream(InputStream in) {
diff --git a/core/src/main/java/nl/andrewl/aos_core/model/Player.java b/core/src/main/java/nl/andrewl/aos_core/model/Player.java
index 408de4c..65be2e3 100644
--- a/core/src/main/java/nl/andrewl/aos_core/model/Player.java
+++ b/core/src/main/java/nl/andrewl/aos_core/model/Player.java
@@ -1,12 +1,47 @@
package nl.andrewl.aos_core.model;
+import nl.andrewl.aos_core.MathUtils;
import org.joml.Vector2f;
import org.joml.Vector3f;
+import static org.joml.Math.*;
+
+/**
+ * Basic information about a player that both the client and server should
+ * know.
+ */
public class Player {
+ /**
+ * The player's position. This is the position of their feet. So if a
+ * player is standing on a block at y=5 (block occupies space from 4 to 5)
+ * then the player's y coordinate is y=6.0. The x and z coordinates are
+ * simply the center of the player.
+ */
private final Vector3f position;
+
+ /**
+ * The player's velocity in each of the coordinate axes.
+ */
private final Vector3f velocity;
+
+ /**
+ * The player's orientation. The x component refers to rotation about the
+ * vertical axis, and the y component refers to rotation about the
+ * horizontal axis. The x component is limited to between 0 and 2 PI, where
+ * x=0 means the player is looking towards the +Z axis. x increases in a
+ * counterclockwise fashion.
+ * The y component is limited to between 0 and PI, with y=0 looking
+ * straight down, and y=PI looking straight up.
+ */
private final Vector2f orientation;
+
+ /**
+ * A vector that's internally re-computed each time the player's
+ * orientation changes, and represents unit vector pointing in the
+ * direction the player is looking.
+ */
+ private final Vector3f viewVector;
+
private final String username;
private final int id;
@@ -14,6 +49,7 @@ public class Player {
this.position = new Vector3f();
this.velocity = new Vector3f();
this.orientation = new Vector2f();
+ this.viewVector = new Vector3f();
this.id = id;
this.username = username;
}
@@ -22,14 +58,28 @@ public class Player {
return position;
}
+ public void setPosition(Vector3f position) {
+ this.position.set(position);
+ }
+
public Vector3f getVelocity() {
return velocity;
}
+ public void setVelocity(Vector3f velocity) {
+ this.velocity.set(velocity);
+ }
+
public Vector2f getOrientation() {
return orientation;
}
+ public void setOrientation(float x, float y) {
+ orientation.set(MathUtils.normalize(x, 0, PI * 2), MathUtils.clamp(y, 0, (float) PI));
+ y = orientation.y + (float) PI / 2f;
+ viewVector.set(sin(orientation.x) * cos(y), -sin(y), cos(orientation.x) * cos(y)).normalize();
+ }
+
public String getUsername() {
return username;
}
@@ -37,4 +87,8 @@ public class Player {
public int getId() {
return id;
}
+
+ public Vector3f getViewVector() {
+ return viewVector;
+ }
}
diff --git a/core/src/main/java/nl/andrewl/aos_core/net/udp/ClientInputState.java b/core/src/main/java/nl/andrewl/aos_core/net/udp/ClientInputState.java
new file mode 100644
index 0000000..3775eb0
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/aos_core/net/udp/ClientInputState.java
@@ -0,0 +1,18 @@
+package nl.andrewl.aos_core.net.udp;
+
+import nl.andrewl.record_net.Message;
+
+/**
+ * A message that' sent periodically by the client when the player's input
+ * changes.
+ */
+public record ClientInputState(
+ int clientId,
+ boolean forward,
+ boolean backward,
+ boolean left,
+ boolean right,
+ boolean jumping,
+ boolean crouching,
+ boolean sprinting
+) implements Message {}
diff --git a/core/src/main/java/nl/andrewl/aos_core/net/udp/ClientOrientationState.java b/core/src/main/java/nl/andrewl/aos_core/net/udp/ClientOrientationState.java
new file mode 100644
index 0000000..bb373d4
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/aos_core/net/udp/ClientOrientationState.java
@@ -0,0 +1,14 @@
+package nl.andrewl.aos_core.net.udp;
+
+import nl.andrewl.record_net.Message;
+
+/**
+ * A message sent by clients when they update their player's orientation.
+ * @param clientId The client's id.
+ * @param x The rotation about the vertical axis.
+ * @param y The rotation about the horizontal axis.
+ */
+public record ClientOrientationState(
+ int clientId,
+ float x, float y
+) implements Message {}
diff --git a/core/src/main/java/nl/andrewl/aos_core/net/udp/PlayerUpdateMessage.java b/core/src/main/java/nl/andrewl/aos_core/net/udp/PlayerUpdateMessage.java
new file mode 100644
index 0000000..86f4a4f
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/aos_core/net/udp/PlayerUpdateMessage.java
@@ -0,0 +1,24 @@
+package nl.andrewl.aos_core.net.udp;
+
+import nl.andrewl.aos_core.model.Player;
+import nl.andrewl.record_net.Message;
+
+/**
+ * This message is sent by the server to clients whenever a player has updated
+ * in some way, like movement or orientation or held items.
+ */
+public record PlayerUpdateMessage(
+ int clientId,
+ float px, float py, float pz,
+ float vx, float vy, float vz,
+ float ox, float oy
+) implements Message {
+ public PlayerUpdateMessage(Player player) {
+ this(
+ player.getId(),
+ player.getPosition().x, player.getPosition().y, player.getPosition().z,
+ player.getVelocity().x, player.getVelocity().y, player.getVelocity().z,
+ player.getOrientation().x, player.getOrientation().y
+ );
+ }
+}
diff --git a/core/src/main/resources/log4j2.properties b/core/src/main/resources/log4j2.properties
new file mode 100644
index 0000000..0f8dfbe
--- /dev/null
+++ b/core/src/main/resources/log4j2.properties
@@ -0,0 +1,9 @@
+appenders = console
+appender.console.type = Console
+appender.console.name = STDOUT
+appender.console.layout.type = PatternLayout
+appender.console.layout.pattern = [%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n
+
+rootLogger.level = debug
+rootLogger.appenderRefs = stdout
+rootLogger.appenderRef.stdout.ref = STDOUT
\ No newline at end of file
diff --git a/server/src/main/java/nl/andrewl/aos2_server/ClientCommunicationHandler.java b/server/src/main/java/nl/andrewl/aos2_server/ClientCommunicationHandler.java
index b7c34b8..0758a65 100644
--- a/server/src/main/java/nl/andrewl/aos2_server/ClientCommunicationHandler.java
+++ b/server/src/main/java/nl/andrewl/aos2_server/ClientCommunicationHandler.java
@@ -1,11 +1,15 @@
package nl.andrewl.aos2_server;
import nl.andrewl.aos_core.Net;
-import nl.andrewl.aos_core.model.Player;
+import nl.andrewl.aos_core.model.Chunk;
import nl.andrewl.aos_core.net.*;
+import nl.andrewl.aos_core.net.udp.PlayerUpdateMessage;
import nl.andrewl.record_net.Message;
import nl.andrewl.record_net.util.ExtendedDataInputStream;
import nl.andrewl.record_net.util.ExtendedDataOutputStream;
+import org.joml.Vector3i;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.DatagramPacket;
@@ -22,6 +26,8 @@ import java.net.Socket;
* from them.
*/
public class ClientCommunicationHandler {
+ private static final Logger log = LoggerFactory.getLogger(ClientCommunicationHandler.class);
+
private final Server server;
private final Socket socket;
private final DatagramSocket datagramSocket;
@@ -29,8 +35,8 @@ public class ClientCommunicationHandler {
private final ExtendedDataOutputStream out;
private InetAddress clientAddress;
- private int clientUdpPort;
- private Player player;
+ private int clientUdpPort = -1;
+ private ServerPlayer player;
public ClientCommunicationHandler(Server server, Socket socket, DatagramSocket datagramSocket) throws IOException {
this.server = server;
@@ -59,7 +65,13 @@ public class ClientCommunicationHandler {
}
private void handleTcpMessage(Message msg) {
- System.out.println("Message received from client " + player.getUsername() + ": " + msg);
+ log.debug("Received TCP message from client \"{}\": {}", player.getUsername(), msg.toString());
+ if (msg instanceof ChunkHashMessage hashMessage) {
+ Chunk chunk = server.getWorld().getChunkAt(new Vector3i(hashMessage.cx(), hashMessage.cy(), hashMessage.cz()));
+ if (chunk != null && hashMessage.hash() != chunk.blockHash()) {
+ sendTcpMessage(new ChunkDataMessage(chunk));
+ }
+ }
}
public void establishConnection() throws IOException {
@@ -74,19 +86,17 @@ public class ClientCommunicationHandler {
socket.setSoTimeout(0);
this.clientAddress = socket.getInetAddress();
connectionEstablished = true;
- this.player = server.registerPlayer(this, connectMsg.username());
+ this.player = server.getPlayerManager().register(this, connectMsg.username());
Net.write(new ConnectAcceptMessage(player.getId()), out);
- System.out.println("Sent connect accept message.");
+ log.debug("Sent connect accept message.");
- System.out.println("Sending world data...");
for (var chunk : server.getWorld().getChunkMap().values()) {
sendTcpMessage(new ChunkDataMessage(chunk));
}
- System.out.println("Sent all world data.");
// Initiate a TCP receiver thread to accept incoming messages from the client.
TcpReceiver tcpReceiver = new TcpReceiver(in, this::handleTcpMessage)
- .withShutdownHook(() -> server.deregisterPlayer(this.player));
+ .withShutdownHook(() -> server.getPlayerManager().deregister(this.player));
new Thread(tcpReceiver).start();
}
} catch (IOException e) {
@@ -100,7 +110,7 @@ public class ClientCommunicationHandler {
} catch (IOException e) {
e.printStackTrace();
}
- System.out.println("Player couldn't connect after " + attempts + " attempts. Aborting.");
+ log.warn("Player couldn't connect after {} attempts. Aborting connection.", attempts);
socket.close();
}
}
@@ -128,9 +138,11 @@ public class ClientCommunicationHandler {
public void sendDatagramPacket(DatagramPacket packet) {
try {
- packet.setAddress(clientAddress);
- packet.setPort(clientUdpPort);
- datagramSocket.send(packet);
+ if (clientUdpPort != -1) {
+ packet.setAddress(clientAddress);
+ packet.setPort(clientUdpPort);
+ datagramSocket.send(packet);
+ }
} catch (IOException e) {
e.printStackTrace();
}
diff --git a/server/src/main/java/nl/andrewl/aos2_server/PlayerManager.java b/server/src/main/java/nl/andrewl/aos2_server/PlayerManager.java
new file mode 100644
index 0000000..c1cbb46
--- /dev/null
+++ b/server/src/main/java/nl/andrewl/aos2_server/PlayerManager.java
@@ -0,0 +1,87 @@
+package nl.andrewl.aos2_server;
+
+import nl.andrewl.aos_core.Net;
+import nl.andrewl.aos_core.net.udp.DatagramInit;
+import nl.andrewl.aos_core.net.udp.PlayerUpdateMessage;
+import nl.andrewl.record_net.Message;
+import org.joml.Vector3f;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.util.*;
+
+/**
+ * This component is responsible for managing the set of players connected to
+ * the server.
+ */
+public class PlayerManager {
+ private static final Logger log = LoggerFactory.getLogger(PlayerManager.class);
+
+ private final Map players = new HashMap<>();
+ private final Map clientHandlers = new HashMap<>();
+ private int nextClientId = 1;
+
+ public synchronized ServerPlayer register(ClientCommunicationHandler handler, String username) {
+ ServerPlayer player = new ServerPlayer(nextClientId++, username);
+ players.put(player.getId(), player);
+ clientHandlers.put(player.getId(), handler);
+ log.info("Registered player \"{}\" with id {}", player.getUsername(), player.getId());
+ player.setPosition(new Vector3f(0, 64, 0));
+ broadcastUdpMessage(new PlayerUpdateMessage(player));
+ return player;
+ }
+
+ public synchronized void deregister(ServerPlayer player) {
+ ClientCommunicationHandler handler = clientHandlers.get(player.getId());
+ if (handler != null) handler.shutdown();
+ players.remove(player.getId());
+ clientHandlers.remove(player.getId());
+ log.info("Deregistered player \"{}\" with id {}", player.getUsername(), player.getId());
+ }
+
+ public synchronized void deregisterAll() {
+ Set playersToDeregister = new HashSet<>(getPlayers());
+ for (var player : playersToDeregister) {
+ deregister(player);
+ }
+ }
+
+ public ServerPlayer getPlayer(int id) {
+ return players.get(id);
+ }
+
+ public Collection getPlayers() {
+ return Collections.unmodifiableCollection(players.values());
+ }
+
+ public ClientCommunicationHandler getHandler(int id) {
+ return clientHandlers.get(id);
+ }
+
+ public Collection getHandlers() {
+ return Collections.unmodifiableCollection(clientHandlers.values());
+ }
+
+ public void handleUdpInit(DatagramInit init, DatagramPacket packet) {
+ var handler = getHandler(init.clientId());
+ if (handler != null) {
+ handler.setClientUdpPort(packet.getPort());
+ handler.sendDatagramPacket(init);
+ log.debug("Echoed player \"{}\"'s UDP init packet.", getPlayer(init.clientId()).getUsername());
+ }
+ }
+
+ public void broadcastUdpMessage(Message msg) {
+ try {
+ byte[] data = Net.write(msg);
+ DatagramPacket packet = new DatagramPacket(data, data.length);
+ for (var handler : getHandlers()) {
+ handler.sendDatagramPacket(packet);
+ }
+ } catch (IOException e) {
+ log.warn("An error occurred while broadcasting a UDP message.", e);
+ }
+ }
+}
diff --git a/server/src/main/java/nl/andrewl/aos2_server/Server.java b/server/src/main/java/nl/andrewl/aos2_server/Server.java
index b97e34d..84459b9 100644
--- a/server/src/main/java/nl/andrewl/aos2_server/Server.java
+++ b/server/src/main/java/nl/andrewl/aos2_server/Server.java
@@ -1,37 +1,39 @@
package nl.andrewl.aos2_server;
import nl.andrewl.aos_core.model.Chunk;
-import nl.andrewl.aos_core.model.Player;
import nl.andrewl.aos_core.model.World;
import nl.andrewl.aos_core.net.UdpReceiver;
+import nl.andrewl.aos_core.net.udp.ClientInputState;
+import nl.andrewl.aos_core.net.udp.ClientOrientationState;
import nl.andrewl.aos_core.net.udp.DatagramInit;
+import nl.andrewl.aos_core.net.udp.PlayerUpdateMessage;
import nl.andrewl.record_net.Message;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.*;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Map;
import java.util.Random;
import java.util.concurrent.ForkJoinPool;
public class Server implements Runnable {
+ private static final Logger log = LoggerFactory.getLogger(Server.class);
+
private final ServerSocket serverSocket;
private final DatagramSocket datagramSocket;
private volatile boolean running;
- private int nextClientId = 1;
- private final Map players;
- private final Map playerClientHandlers;
+ private final PlayerManager playerManager;
private final World world;
+ private final WorldUpdater worldUpdater;
public Server() throws IOException {
this.serverSocket = new ServerSocket(24464, 5);
this.serverSocket.setReuseAddress(true);
this.datagramSocket = new DatagramSocket(24464);
this.datagramSocket.setReuseAddress(true);
- this.players = new HashMap<>();
- this.playerClientHandlers = new HashMap<>();
+ this.playerManager = new PlayerManager();
+ this.worldUpdater = new WorldUpdater(this, 20);
// Generate world. TODO: do this elsewhere.
Random rand = new Random(1);
@@ -53,14 +55,14 @@ public class Server implements Runnable {
public void run() {
running = true;
new Thread(new UdpReceiver(datagramSocket, this::handleUdpMessage)).start();
- System.out.println("Started AOS2-Server on TCP/UDP port " + serverSocket.getLocalPort() + "; now accepting connections.");
+ new Thread(worldUpdater).start();
+ log.info("Started AoS2 Server on TCP/UDP port {}; now accepting connections.", serverSocket.getLocalPort());
while (running) {
acceptClientConnection();
}
- for (var player : players.values()) {
- deregisterPlayer(player);
- }
- datagramSocket.close();
+ playerManager.deregisterAll();
+ worldUpdater.shutdown();
+ datagramSocket.close(); // Shuts down the UdpReceiver.
try {
serverSocket.close();
} catch (IOException e) {
@@ -69,44 +71,27 @@ public class Server implements Runnable {
}
public void handleUdpMessage(Message msg, DatagramPacket packet) {
- // Echo any init message from known clients.
if (msg instanceof DatagramInit init) {
- var handler = getHandler(init.clientId());
- if (handler != null) {
- handler.setClientUdpPort(packet.getPort());
- handler.sendDatagramPacket(msg);
+ playerManager.handleUdpInit(init, packet);
+ } else if (msg instanceof ClientInputState inputState) {
+ ServerPlayer player = playerManager.getPlayer(inputState.clientId());
+ if (player != null) {
+ player.setLastInputState(inputState);
+ }
+ } else if (msg instanceof ClientOrientationState orientationState) {
+ ServerPlayer player = playerManager.getPlayer(orientationState.clientId());
+ if (player != null) {
+ player.setOrientation(orientationState.x(), orientationState.y());
+ playerManager.broadcastUdpMessage(new PlayerUpdateMessage(player));
}
}
}
- public synchronized Player registerPlayer(ClientCommunicationHandler handler, String username) {
- Player player = new Player(nextClientId++, username);
- players.put(player.getId(), player);
- playerClientHandlers.put(player.getId(), handler);
- System.out.println("Registered player " + username + " with id " + player.getId());
- return player;
- }
-
- public synchronized void deregisterPlayer(Player player) {
- ClientCommunicationHandler handler = playerClientHandlers.get(player.getId());
- handler.shutdown();
- players.remove(player.getId());
- playerClientHandlers.remove(player.getId());
- System.out.println("Deregistered player " + player.getUsername() + " with id " + player.getId());
- }
-
- public ClientCommunicationHandler getHandler(int id) {
- return playerClientHandlers.get(id);
- }
-
- public World getWorld() {
- return world;
- }
-
private void acceptClientConnection() {
try {
Socket clientSocket = serverSocket.accept();
var handler = new ClientCommunicationHandler(this, clientSocket, datagramSocket);
+ // Establish the connection in a separate thread so that we can continue accepting clients.
ForkJoinPool.commonPool().submit(() -> {
try {
handler.establishConnection();
@@ -122,6 +107,14 @@ public class Server implements Runnable {
}
}
+ public World getWorld() {
+ return world;
+ }
+
+ public PlayerManager getPlayerManager() {
+ return playerManager;
+ }
+
public static void main(String[] args) throws IOException {
new Server().run();
}
diff --git a/server/src/main/java/nl/andrewl/aos2_server/ServerPlayer.java b/server/src/main/java/nl/andrewl/aos2_server/ServerPlayer.java
new file mode 100644
index 0000000..380481b
--- /dev/null
+++ b/server/src/main/java/nl/andrewl/aos2_server/ServerPlayer.java
@@ -0,0 +1,22 @@
+package nl.andrewl.aos2_server;
+
+import nl.andrewl.aos_core.model.Player;
+import nl.andrewl.aos_core.net.udp.ClientInputState;
+
+public class ServerPlayer extends Player {
+ private ClientInputState lastInputState;
+
+ public ServerPlayer(int id, String username) {
+ super(id, username);
+ // Initialize with a default state of no input.
+ lastInputState = new ClientInputState(id, false, false, false, false, false, false, false);
+ }
+
+ public ClientInputState getLastInputState() {
+ return lastInputState;
+ }
+
+ public void setLastInputState(ClientInputState inputState) {
+ this.lastInputState = inputState;
+ }
+}
diff --git a/server/src/main/java/nl/andrewl/aos2_server/WorldUpdater.java b/server/src/main/java/nl/andrewl/aos2_server/WorldUpdater.java
new file mode 100644
index 0000000..96b663c
--- /dev/null
+++ b/server/src/main/java/nl/andrewl/aos2_server/WorldUpdater.java
@@ -0,0 +1,103 @@
+package nl.andrewl.aos2_server;
+
+import nl.andrewl.aos_core.net.udp.PlayerUpdateMessage;
+import org.joml.Matrix4f;
+import org.joml.Vector3f;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A runnable to run as a separate thread, to periodically update the server's
+ * world as players perform actions. This is essentially the "core" of the
+ * game engine, as it controls the game's main update pattern.
+ */
+public class WorldUpdater implements Runnable {
+ private static final Logger log = LoggerFactory.getLogger(WorldUpdater.class);
+
+ private final Server server;
+ private final float ticksPerSecond;
+ private volatile boolean running;
+
+ public WorldUpdater(Server server, float ticksPerSecond) {
+ this.server = server;
+ this.ticksPerSecond = ticksPerSecond;
+ }
+
+ public void shutdown() {
+ running = false;
+ }
+
+ @Override
+ public void run() {
+ final long nsPerTick = (long) Math.floor((1.0 / ticksPerSecond) * 1_000_000_000.0);
+ log.debug("Running world updater at {} ticks per second, or {} ns per tick.", ticksPerSecond, nsPerTick);
+ running = true;
+ while (running) {
+ long start = System.nanoTime();
+ tick();
+ long elapsedNs = System.nanoTime() - start;
+ if (elapsedNs > nsPerTick) {
+ log.warn("Took {} ns to do one tick, which is more than the desired {} ns per tick.", elapsedNs, nsPerTick);
+ } else {
+ long sleepTime = nsPerTick - elapsedNs;
+ long ms = sleepTime / 1_000_000;
+ int nanos = (int) (sleepTime % 1_000_000);
+ try {
+ Thread.sleep(ms, nanos);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+ }
+
+ private void tick() {
+ for (var player : server.getPlayerManager().getPlayers()) {
+ updatePlayerMovement(player);
+ }
+ }
+
+ private void updatePlayerMovement(ServerPlayer player) {
+ boolean updated = false;
+ var v = player.getVelocity();
+ var p = player.getPosition();
+
+ // Apply deceleration to the player before computing any input-derived acceleration.
+ if (v.length() > 0) {
+ Vector3f deceleration = new Vector3f(v).negate().normalize().mul(0.1f);
+ v.add(deceleration);
+ if (v.length() < 0.1f) {
+ v.set(0);
+ }
+ updated = true;
+ }
+
+ Vector3f a = new Vector3f();
+ var inputState = player.getLastInputState();
+ if (inputState.forward()) a.z -= 1;
+ if (inputState.backward()) a.z += 1;
+ if (inputState.left()) a.x -= 1;
+ if (inputState.right()) a.x += 1;
+ if (inputState.jumping()) a.y += 1; // TODO: check if on ground.
+ if (inputState.crouching()) a.y -= 1; // TODO: do crouching instead of down.
+ if (a.lengthSquared() > 0) {
+ a.normalize();
+ Matrix4f moveTransform = new Matrix4f();
+ moveTransform.rotate(player.getOrientation().x, new Vector3f(0, 1, 0));
+ moveTransform.transformDirection(a);
+ v.add(a);
+ final float maxSpeed = 0.25f; // Blocks per tick.
+ if (v.length() > maxSpeed) v.normalize(maxSpeed);
+ updated = true;
+ }
+
+ if (v.lengthSquared() > 0) {
+ p.add(v);
+ updated = true;
+ }
+
+ if (updated) {
+ server.getPlayerManager().broadcastUdpMessage(new PlayerUpdateMessage(player));
+ }
+ }
+}