Added server-controlled physics base.

This commit is contained in:
Andrew Lalis 2022-07-07 16:51:29 +02:00
parent e66c94680d
commit f2b0e09979
17 changed files with 479 additions and 86 deletions

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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());
}
}
}
}

View File

@ -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");

View File

@ -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) {

View File

@ -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>

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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 {}

View File

@ -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 {}

View File

@ -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
);
}
}

View File

@ -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

View File

@ -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();
}

View File

@ -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);
}
}
}

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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));
}
}
}