Added server-controlled physics base.
This commit is contained in:
parent
e66c94680d
commit
f2b0e09979
|
@ -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,10 +67,12 @@ public class Camera implements GLFWCursorPosCallbackI {
|
|||
}
|
||||
|
||||
public void setPosition(float x, float y, float 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) {
|
||||
orientation.set(
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
@ -31,7 +36,7 @@ public class CommunicationHandler {
|
|||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
15
core/pom.xml
15
core/pom.xml
|
@ -33,7 +33,7 @@
|
|||
<dependency>
|
||||
<groupId>com.github.andrewlalis</groupId>
|
||||
<artifactId>record-net</artifactId>
|
||||
<version>v1.2.1</version>
|
||||
<version>v1.3.4</version>
|
||||
</dependency>
|
||||
<!-- https://github.com/OpenHFT/Zero-Allocation-Hashing -->
|
||||
<dependency>
|
||||
|
@ -41,6 +41,19 @@
|
|||
<artifactId>zero-allocation-hashing</artifactId>
|
||||
<version>0.15</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>1.7.36</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j-impl -->
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-slf4j-impl</artifactId>
|
||||
<version>2.18.0</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
|
||||
<dependency>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {}
|
|
@ -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 {}
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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 {
|
||||
if (clientUdpPort != -1) {
|
||||
packet.setAddress(clientAddress);
|
||||
packet.setPort(clientUdpPort);
|
||||
datagramSocket.send(packet);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
|
|
@ -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<Integer, ServerPlayer> players = new HashMap<>();
|
||||
private final Map<Integer, ClientCommunicationHandler> 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<ServerPlayer> playersToDeregister = new HashSet<>(getPlayers());
|
||||
for (var player : playersToDeregister) {
|
||||
deregister(player);
|
||||
}
|
||||
}
|
||||
|
||||
public ServerPlayer getPlayer(int id) {
|
||||
return players.get(id);
|
||||
}
|
||||
|
||||
public Collection<ServerPlayer> getPlayers() {
|
||||
return Collections.unmodifiableCollection(players.values());
|
||||
}
|
||||
|
||||
public ClientCommunicationHandler getHandler(int id) {
|
||||
return clientHandlers.get(id);
|
||||
}
|
||||
|
||||
public Collection<ClientCommunicationHandler> 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Integer, Player> players;
|
||||
private final Map<Integer, ClientCommunicationHandler> 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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue