From f2b0e0997933a8ee3724f2478eb3bfd7a1aa847a Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Thu, 7 Jul 2022 16:51:29 +0200 Subject: [PATCH] Added server-controlled physics base. --- .../java/nl/andrewl/aos2_client/Camera.java | 26 +++-- .../java/nl/andrewl/aos2_client/Client.java | 42 +++++-- .../aos2_client/CommunicationHandler.java | 19 +++- .../aos2_client/render/ChunkRenderer.java | 2 - .../aos2_client/render/WindowUtils.java | 7 +- core/pom.xml | 15 ++- .../main/java/nl/andrewl/aos_core/Net.java | 6 +- .../nl/andrewl/aos_core/model/Player.java | 54 +++++++++ .../aos_core/net/udp/ClientInputState.java | 18 +++ .../net/udp/ClientOrientationState.java | 14 +++ .../aos_core/net/udp/PlayerUpdateMessage.java | 24 ++++ core/src/main/resources/log4j2.properties | 9 ++ .../ClientCommunicationHandler.java | 38 ++++--- .../nl/andrewl/aos2_server/PlayerManager.java | 87 +++++++++++++++ .../java/nl/andrewl/aos2_server/Server.java | 79 ++++++-------- .../nl/andrewl/aos2_server/ServerPlayer.java | 22 ++++ .../nl/andrewl/aos2_server/WorldUpdater.java | 103 ++++++++++++++++++ 17 files changed, 479 insertions(+), 86 deletions(-) create mode 100644 core/src/main/java/nl/andrewl/aos_core/net/udp/ClientInputState.java create mode 100644 core/src/main/java/nl/andrewl/aos_core/net/udp/ClientOrientationState.java create mode 100644 core/src/main/java/nl/andrewl/aos_core/net/udp/PlayerUpdateMessage.java create mode 100644 core/src/main/resources/log4j2.properties create mode 100644 server/src/main/java/nl/andrewl/aos2_server/PlayerManager.java create mode 100644 server/src/main/java/nl/andrewl/aos2_server/ServerPlayer.java create mode 100644 server/src/main/java/nl/andrewl/aos2_server/WorldUpdater.java 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)); + } + } +}