From 4ef8e88e81fa289e82f0381c3cffc0d7d6556205 Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Sat, 9 Jul 2022 16:10:19 +0200 Subject: [PATCH] Added more preparations for true multiplayer functionality. --- .../java/nl/andrewl/aos2_client/Camera.java | 8 +- .../java/nl/andrewl/aos2_client/Client.java | 12 +- .../control/PlayerViewCursorCallback.java | 4 +- .../andrewl/aos2_client/render/ChunkMesh.java | 7 +- .../render/ChunkMeshGenerator.java | 35 ++- .../aos2_client/render/ChunkRenderer.java | 5 +- .../aos2_client/render/GameRenderer.java | 7 +- .../nl/andrewl/aos2_client/render/Mesh.java | 70 ++++++ .../andrewl/aos2_client/render/MeshData.java | 6 + .../render/PlayerMeshGenerator.java | 26 +++ .../main/java/nl/andrewl/aos_core/Net.java | 2 + .../andrewl/aos_core/model/ColorPalette.java | 50 +++++ .../nl/andrewl/aos_core/model/Player.java | 12 +- .../java/nl/andrewl/aos_core/model/World.java | 68 +++--- .../aos_core/net/PlayerJoinMessage.java | 14 ++ .../aos_core/net/PlayerLeaveMessage.java | 10 + .../nl/andrewl/aos_core/model/WorldTest.java | 1 + .../nl/andrewl/aos2_server/PlayerManager.java | 14 ++ .../java/nl/andrewl/aos2_server/Server.java | 76 +++++-- .../nl/andrewl/aos2_server/ServerPlayer.java | 210 ++++++++++++++++++ .../nl/andrewl/aos2_server/WorldUpdater.java | 5 +- 21 files changed, 562 insertions(+), 80 deletions(-) create mode 100644 client/src/main/java/nl/andrewl/aos2_client/render/Mesh.java create mode 100644 client/src/main/java/nl/andrewl/aos2_client/render/MeshData.java create mode 100644 client/src/main/java/nl/andrewl/aos2_client/render/PlayerMeshGenerator.java create mode 100644 core/src/main/java/nl/andrewl/aos_core/model/ColorPalette.java create mode 100644 core/src/main/java/nl/andrewl/aos_core/net/PlayerJoinMessage.java create mode 100644 core/src/main/java/nl/andrewl/aos_core/net/PlayerLeaveMessage.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 0a1b6b2..bf9c6e3 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/Camera.java +++ b/client/src/main/java/nl/andrewl/aos2_client/Camera.java @@ -89,7 +89,13 @@ public class Camera { setOrientation((float) Math.toRadians(x), (float) Math.toRadians(y)); } - private void updateViewTransform() { + public void interpolatePosition(float dt) { + Vector3f movement = new Vector3f(velocity).mul(dt); + position.add(movement); + updateViewTransform(); + } + + public void updateViewTransform() { viewTransform.identity(); viewTransform.rotate(-orientation.y + ((float) Math.PI / 2), RIGHT); viewTransform.rotate(-orientation.x, UP); 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 719fec7..96fce21 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/Client.java +++ b/client/src/main/java/nl/andrewl/aos2_client/Client.java @@ -8,7 +8,6 @@ import nl.andrewl.aos_core.model.World; import nl.andrewl.aos_core.net.ChunkDataMessage; 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; @@ -27,15 +26,15 @@ public class Client implements Runnable { private final GameRenderer gameRenderer; private int clientId; - private final World world; + private final ClientWorld world; public Client(InetAddress serverAddress, int serverPort, String username) { this.serverAddress = serverAddress; this.serverPort = serverPort; this.username = username; this.communicationHandler = new CommunicationHandler(this); - this.gameRenderer = new GameRenderer(); - this.world = new World(); + this.world = new ClientWorld(); + this.gameRenderer = new GameRenderer(world); } @Override @@ -58,10 +57,8 @@ public class Client implements Runnable { while (!gameRenderer.windowShouldClose()) { long now = System.currentTimeMillis(); float dt = (now - lastFrameAt) / 1000f; + gameRenderer.getCamera().interpolatePosition(dt); gameRenderer.draw(); - // Interpolate camera movement to make the game feel smooth. - Vector3f camMovement = new Vector3f(gameRenderer.getCamera().getVelocity()).mul(dt); - gameRenderer.getCamera().getPosition().add(camMovement); lastFrameAt = now; } gameRenderer.freeWindow(); @@ -86,6 +83,7 @@ public class Client implements Runnable { if (playerUpdate.clientId() == clientId) { gameRenderer.getCamera().setPosition(playerUpdate.px(), playerUpdate.py() + 1.8f, playerUpdate.pz()); gameRenderer.getCamera().setVelocity(playerUpdate.vx(), playerUpdate.vy(), playerUpdate.vz()); + // TODO: Unload far away chunks and request close chunks we don't have. } } } diff --git a/client/src/main/java/nl/andrewl/aos2_client/control/PlayerViewCursorCallback.java b/client/src/main/java/nl/andrewl/aos2_client/control/PlayerViewCursorCallback.java index e1fffda..7ee4afe 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/control/PlayerViewCursorCallback.java +++ b/client/src/main/java/nl/andrewl/aos2_client/control/PlayerViewCursorCallback.java @@ -5,6 +5,8 @@ import nl.andrewl.aos2_client.CommunicationHandler; import nl.andrewl.aos_core.net.udp.ClientOrientationState; import org.lwjgl.glfw.GLFWCursorPosCallbackI; +import java.util.concurrent.ForkJoinPool; + import static org.lwjgl.glfw.GLFW.glfwGetCursorPos; public class PlayerViewCursorCallback implements GLFWCursorPosCallbackI { @@ -40,7 +42,7 @@ public class PlayerViewCursorCallback implements GLFWCursorPosCallbackI { camera.setOrientation(camera.getOrientation().x - dx * mouseCursorSensitivity, camera.getOrientation().y - dy * mouseCursorSensitivity); long now = System.currentTimeMillis(); if (lastOrientationUpdateSentAt + ORIENTATION_UPDATE_LIMIT < now) { - comm.sendDatagramPacket(new ClientOrientationState(comm.getClientId(), camera.getOrientation().x, camera.getOrientation().y)); + ForkJoinPool.commonPool().submit(() -> comm.sendDatagramPacket(new ClientOrientationState(comm.getClientId(), camera.getOrientation().x, camera.getOrientation().y))); lastOrientationUpdateSentAt = now; } } diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/ChunkMesh.java b/client/src/main/java/nl/andrewl/aos2_client/render/ChunkMesh.java index 23517b2..1221e4e 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/render/ChunkMesh.java +++ b/client/src/main/java/nl/andrewl/aos2_client/render/ChunkMesh.java @@ -1,6 +1,7 @@ package nl.andrewl.aos2_client.render; import nl.andrewl.aos_core.model.Chunk; +import nl.andrewl.aos_core.model.World; import static org.lwjgl.opengl.GL46.*; @@ -16,9 +17,11 @@ public class ChunkMesh { private final int[] positionData; private final Chunk chunk; + private final World world; - public ChunkMesh(Chunk chunk, ChunkMeshGenerator meshGenerator) { + public ChunkMesh(Chunk chunk, World world, ChunkMeshGenerator meshGenerator) { this.chunk = chunk; + this.world = world; this.positionData = new int[]{chunk.getPosition().x, chunk.getPosition().y, chunk.getPosition().z}; this.vboId = glGenBuffers(); @@ -39,7 +42,7 @@ public class ChunkMesh { */ private void loadMesh(ChunkMeshGenerator meshGenerator) { long start = System.nanoTime(); - var meshData = meshGenerator.generateMesh(chunk); + var meshData = meshGenerator.generateMesh(chunk, world); double dur = (System.nanoTime() - start) / 1_000_000.0; this.indexCount = meshData.indexBuffer().limit(); // Print some debug information. diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/ChunkMeshGenerator.java b/client/src/main/java/nl/andrewl/aos2_client/render/ChunkMeshGenerator.java index 6758bdd..a45903a 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/render/ChunkMeshGenerator.java +++ b/client/src/main/java/nl/andrewl/aos2_client/render/ChunkMeshGenerator.java @@ -1,6 +1,7 @@ package nl.andrewl.aos2_client.render; import nl.andrewl.aos_core.model.Chunk; +import nl.andrewl.aos_core.model.World; import org.joml.Vector3f; import org.joml.Vector3i; import org.lwjgl.BufferUtils; @@ -16,16 +17,19 @@ public final class ChunkMeshGenerator { private final FloatBuffer vertexBuffer; private final IntBuffer indexBuffer; - private final Vector3i pos = new Vector3i(); - private final Vector3f color = new Vector3f(); - private final Vector3f norm = new Vector3f(); + private final Vector3i pos = new Vector3i();// Pre-allocated vector to hold current local chunk block position. + private final Vector3f color = new Vector3f();// Pre-allocated vector to hold current block color. + private final Vector3f norm = new Vector3f();// Pre-allocated vector to hold current face normal. + + private final Vector3f checkPos = new Vector3f();// Pre-allocated vector to hold world block position. + private final Vector3i checkUtil = new Vector3i();// Pre-allocated utility vector to give to World for position stuff. public ChunkMeshGenerator() { vertexBuffer = BufferUtils.createFloatBuffer(300_000); indexBuffer = BufferUtils.createIntBuffer(100_000); } - public ChunkMeshData generateMesh(Chunk chunk) { + public ChunkMeshData generateMesh(Chunk chunk, World world) { vertexBuffer.clear(); indexBuffer.clear(); int idx = 0; @@ -34,12 +38,15 @@ public final class ChunkMeshGenerator { int x = pos.x; int y = pos.y; int z = pos.z; + int worldX = Chunk.SIZE * chunk.getPosition().x + x; + int worldY = Chunk.SIZE * chunk.getPosition().y + y; + int worldZ = Chunk.SIZE * chunk.getPosition().z + z; byte block = chunk.getBlocks()[i]; if (block <= 0) { continue; // Don't render empty blocks. } - Chunk.getColor(block, color); + color.set(world.getPalette().getColor(block)); // See /design/block_rendering.svg for a diagram of how these vertices are defined. // var a = new Vector3f(x, y + 1, z + 1); @@ -52,7 +59,8 @@ public final class ChunkMeshGenerator { // var h = new Vector3f(x + 1, y, z); // Top - if (chunk.getBlockAt(x, y + 1, z) == 0) { + checkPos.set(worldX, worldY + 1, worldZ); + if (world.getBlockAt(checkPos, checkUtil) == 0) { norm.set(0, 1, 0); genFace(idx, x, y+1, z+1, // a @@ -63,7 +71,8 @@ public final class ChunkMeshGenerator { idx += 4; } // Bottom - if (chunk.getBlockAt(x, y - 1, z) == 0) { + checkPos.set(worldX, worldY - 1, worldZ); + if (world.getBlockAt(checkPos, checkUtil) == 0) { norm.set(0, -1, 0);// c h g d genFace(idx, x, y, z, // c @@ -74,7 +83,8 @@ public final class ChunkMeshGenerator { idx += 4; } // Positive z - if (chunk.getBlockAt(x, y, z + 1) == 0) { + checkPos.set(worldX, worldY, worldZ + 1); + if (world.getBlockAt(checkPos, checkUtil) == 0) { norm.set(0, 0, 1); genFace(idx, x+1, y+1, z+1, // f @@ -85,7 +95,8 @@ public final class ChunkMeshGenerator { idx += 4; } // Negative z - if (chunk.getBlockAt(x, y, z - 1) == 0) { + checkPos.set(worldX, worldY, worldZ - 1); + if (world.getBlockAt(checkPos, checkUtil) == 0) { norm.set(0, 0, -1); genFace(idx, x, y+1, z, // b @@ -96,7 +107,8 @@ public final class ChunkMeshGenerator { idx += 4; } // Positive x - if (chunk.getBlockAt(x + 1, y, z) == 0) { + checkPos.set(worldX + 1, worldY, worldZ); + if (world.getBlockAt(checkPos, checkUtil) == 0) { norm.set(1, 0, 0); genFace(idx, x+1, y+1, z, // e @@ -107,7 +119,8 @@ public final class ChunkMeshGenerator { idx += 4; } // Negative x - if (chunk.getBlockAt(x - 1, y, z) == 0) { + checkPos.set(worldX - 1, worldY, worldZ); + if (world.getBlockAt(checkPos, checkUtil) == 0) { norm.set(-1, 0, 0); genFace(idx, x, y+1, z+1, // a 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 10d1cf4..fc4d8e3 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 @@ -2,6 +2,7 @@ package nl.andrewl.aos2_client.render; import nl.andrewl.aos2_client.Camera; import nl.andrewl.aos_core.model.Chunk; +import nl.andrewl.aos_core.model.World; import org.joml.Matrix4f; import java.util.ArrayList; @@ -50,9 +51,9 @@ public class ChunkRenderer { glUniformMatrix4fv(projectionTransformUniform, false, projectionTransform.get(new float[16])); } - public void draw(Camera cam) { + public void draw(Camera cam, World world) { while (!meshGenerationQueue.isEmpty()) { - chunkMeshes.add(new ChunkMesh(meshGenerationQueue.remove(), chunkMeshGenerator)); + chunkMeshes.add(new ChunkMesh(meshGenerationQueue.remove(), world, chunkMeshGenerator)); } shaderProgram.use(); glUniformMatrix4fv(viewTransformUniform, false, cam.getViewTransformData()); diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/GameRenderer.java b/client/src/main/java/nl/andrewl/aos2_client/render/GameRenderer.java index 1a025cd..7033b07 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/render/GameRenderer.java +++ b/client/src/main/java/nl/andrewl/aos2_client/render/GameRenderer.java @@ -4,6 +4,7 @@ import nl.andrewl.aos2_client.Camera; import nl.andrewl.aos2_client.control.PlayerInputKeyCallback; import nl.andrewl.aos2_client.control.PlayerViewCursorCallback; import nl.andrewl.aos_core.model.Chunk; +import nl.andrewl.aos_core.model.World; import org.joml.Matrix4f; import org.lwjgl.glfw.Callbacks; import org.lwjgl.glfw.GLFWErrorCallback; @@ -28,6 +29,7 @@ public class GameRenderer { private final ChunkRenderer chunkRenderer; private final Camera camera; + private final World world; private long windowHandle; private GLFWVidMode primaryMonitorSettings; @@ -38,7 +40,8 @@ public class GameRenderer { private final Matrix4f perspectiveTransform; - public GameRenderer() { + public GameRenderer(World world) { + this.world = world; this.chunkRenderer = new ChunkRenderer(); this.camera = new Camera(); this.perspectiveTransform = new Matrix4f(); @@ -140,7 +143,7 @@ public class GameRenderer { public void draw() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - chunkRenderer.draw(camera); + chunkRenderer.draw(camera, world); glfwSwapBuffers(windowHandle); glfwPollEvents(); diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/Mesh.java b/client/src/main/java/nl/andrewl/aos2_client/render/Mesh.java new file mode 100644 index 0000000..e815948 --- /dev/null +++ b/client/src/main/java/nl/andrewl/aos2_client/render/Mesh.java @@ -0,0 +1,70 @@ +package nl.andrewl.aos2_client.render; + +import org.joml.Matrix4f; + +import static org.lwjgl.opengl.GL46.*; + +public class Mesh { + private final int vboId; + private final int vaoId; + private final int eboId; + private int indexCount; + private final Matrix4f transform = new Matrix4f(); + private final float[] transformData = new float[16]; + + public Mesh(MeshData initialData) { + this.vboId = glGenBuffers(); + this.eboId = glGenBuffers(); + this.vaoId = glGenVertexArrays(); + load(initialData); + initVertexArrayAttributes(); + } + + public void load(MeshData data) { + indexCount = data.indexBuffer().limit(); + glBindBuffer(GL_ARRAY_BUFFER, vboId); + glBufferData(GL_ARRAY_BUFFER, data.vertexBuffer(), GL_STATIC_DRAW); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, eboId); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, data.indexBuffer(), GL_STATIC_DRAW); + } + + public Matrix4f getTransform() { + return transform; + } + + public void updateTransform() { + transform.set(transformData); + } + + public float[] getTransformData() { + return transformData; + } + + /** + * Initializes this mesh's vertex array attribute settings. + */ + private void initVertexArrayAttributes() { + glBindVertexArray(vaoId); + // Vertex position floats. + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 3, GL_FLOAT, false, 9 * Float.BYTES, 0); + // Vertex color floats. + glEnableVertexAttribArray(1); + glVertexAttribPointer(1, 3, GL_FLOAT, false, 9 * Float.BYTES, 3 * Float.BYTES); + // Vertex normal floats. + glEnableVertexAttribArray(2); + glVertexAttribPointer(2, 3, GL_FLOAT, false, 9 * Float.BYTES, 6 * Float.BYTES); + } + + public void draw() { + glBindVertexArray(vaoId); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, eboId); + glDrawElements(GL_TRIANGLES, indexCount, GL_UNSIGNED_INT, 0); + } + + public void free() { + glDeleteBuffers(vboId); + glDeleteBuffers(eboId); + glDeleteVertexArrays(vaoId); + } +} diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/MeshData.java b/client/src/main/java/nl/andrewl/aos2_client/render/MeshData.java new file mode 100644 index 0000000..fde20be --- /dev/null +++ b/client/src/main/java/nl/andrewl/aos2_client/render/MeshData.java @@ -0,0 +1,6 @@ +package nl.andrewl.aos2_client.render; + +import java.nio.FloatBuffer; +import java.nio.IntBuffer; + +public record MeshData(FloatBuffer vertexBuffer, IntBuffer indexBuffer) {} diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/PlayerMeshGenerator.java b/client/src/main/java/nl/andrewl/aos2_client/render/PlayerMeshGenerator.java new file mode 100644 index 0000000..db831ca --- /dev/null +++ b/client/src/main/java/nl/andrewl/aos2_client/render/PlayerMeshGenerator.java @@ -0,0 +1,26 @@ +package nl.andrewl.aos2_client.render; + +import org.joml.Vector3f; +import org.joml.Vector3i; +import org.lwjgl.BufferUtils; + +import java.nio.FloatBuffer; +import java.nio.IntBuffer; + +public class PlayerMeshGenerator { + private final FloatBuffer vertexBuffer; + private final IntBuffer indexBuffer; + + private final Vector3i pos = new Vector3i(); + private final Vector3f color = new Vector3f(); + private final Vector3f norm = new Vector3f(); + + public PlayerMeshGenerator() { + vertexBuffer = BufferUtils.createFloatBuffer(1000); + indexBuffer = BufferUtils.createIntBuffer(100); + } + +// public PlayerMesh generateMesh() { +// +// } +} 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 90a27c8..d2151e2 100644 --- a/core/src/main/java/nl/andrewl/aos_core/Net.java +++ b/core/src/main/java/nl/andrewl/aos_core/Net.java @@ -30,6 +30,8 @@ public final class Net { serializer.registerType(8, ClientInputState.class); serializer.registerType(9, ClientOrientationState.class); serializer.registerType(10, PlayerUpdateMessage.class); + serializer.registerType(11, PlayerJoinMessage.class); + serializer.registerType(12, PlayerLeaveMessage.class); } public static ExtendedDataInputStream getInputStream(InputStream in) { diff --git a/core/src/main/java/nl/andrewl/aos_core/model/ColorPalette.java b/core/src/main/java/nl/andrewl/aos_core/model/ColorPalette.java new file mode 100644 index 0000000..2c42fc3 --- /dev/null +++ b/core/src/main/java/nl/andrewl/aos_core/model/ColorPalette.java @@ -0,0 +1,50 @@ +package nl.andrewl.aos_core.model; + +import org.joml.Vector3f; + +import java.awt.*; + +/** + * A palette of 127 colors that can be used for coloring a world. + */ +public class ColorPalette { + public static final int MAX_COLORS = 127; + + private final Vector3f[] colors = new Vector3f[MAX_COLORS]; + + public ColorPalette() { + for (int i = 0; i < MAX_COLORS; i++) { + colors[i] = new Vector3f(); + } + } + + public Vector3f getColor(byte value) { + if (value < 0) return null; + return colors[value - 1]; + } + + public void setColor(byte value, float r, float g, float b) { + if (value < 0) return; + colors[value - 1].set(r, g, b); + } + + public static ColorPalette grayscale() { + ColorPalette palette = new ColorPalette(); + for (int i = 0; i < MAX_COLORS; i++) { + palette.colors[i].set((float) i / MAX_COLORS); + } + return palette; + } + + public static ColorPalette rainbow() { + ColorPalette palette = new ColorPalette(); + for (int i = 0; i < MAX_COLORS; i++) { + Color c = Color.getHSBColor((float) i / MAX_COLORS, 0.8f, 0.8f); + float[] values = c.getRGBColorComponents(null); + palette.colors[i].x = values[0]; + palette.colors[i].y = values[1]; + palette.colors[i].z = values[2]; + } + return palette; + } +} 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 65be2e3..a56fecd 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 @@ -17,12 +17,12 @@ public class Player { * 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; + protected final Vector3f position; /** * The player's velocity in each of the coordinate axes. */ - private final Vector3f velocity; + protected final Vector3f velocity; /** * The player's orientation. The x component refers to rotation about the @@ -33,17 +33,17 @@ public class Player { * 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; + protected 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; + protected final Vector3f viewVector; - private final String username; - private final int id; + protected final String username; + protected final int id; public Player(int id, String username) { this.position = new Vector3f(); diff --git a/core/src/main/java/nl/andrewl/aos_core/model/World.java b/core/src/main/java/nl/andrewl/aos_core/model/World.java index d97063b..e88da01 100644 --- a/core/src/main/java/nl/andrewl/aos_core/model/World.java +++ b/core/src/main/java/nl/andrewl/aos_core/model/World.java @@ -14,25 +14,40 @@ import java.util.Map; */ public class World { protected final Map chunkMap = new HashMap<>(); + protected final ColorPalette palette = ColorPalette.rainbow(); public void addChunk(Chunk chunk) { chunkMap.put(chunk.getPosition(), chunk); } + public void removeChunk(Vector3i chunkPos) { + chunkMap.remove(chunkPos); + } + public Map getChunkMap() { return chunkMap; } + public ColorPalette getPalette() { + return palette; + } + public byte getBlockAt(Vector3f pos) { - Vector3i chunkPos = getChunkPosAt(pos); - Chunk chunk = chunkMap.get(chunkPos); + return getBlockAt(pos, new Vector3i()); + } + + public byte getBlockAt(Vector3f pos, Vector3i util) { + getChunkPosAt(pos, util); + Chunk chunk = chunkMap.get(util); if (chunk == null) return 0; - Vector3i blockPos = new Vector3i( - (int) Math.floor(pos.x - chunkPos.x * Chunk.SIZE), - (int) Math.floor(pos.y - chunkPos.y * Chunk.SIZE), - (int) Math.floor(pos.z - chunkPos.z * Chunk.SIZE) - ); - return chunk.getBlockAt(blockPos); + util.x = (int) Math.floor(pos.x - util.x * Chunk.SIZE); + util.y = (int) Math.floor(pos.y - util.y * Chunk.SIZE); + util.z = (int) Math.floor(pos.z - util.z * Chunk.SIZE); + return chunk.getBlockAt(util); + } + + public byte getBlockAt(float x, float y, float z) { + return getBlockAt(new Vector3f(x, y, z)); } public void setBlockAt(Vector3f pos, byte block) { @@ -47,18 +62,18 @@ public class World { chunk.setBlockAt(blockPos.x, blockPos.y, blockPos.z, block); } - public byte getBlockAt(int x, int y, int z) { - int chunkX = x / Chunk.SIZE; - int localX = x % Chunk.SIZE; - int chunkY = y / Chunk.SIZE; - int localY = y % Chunk.SIZE; - int chunkZ = z / Chunk.SIZE; - int localZ = z % Chunk.SIZE; - Vector3i chunkPos = new Vector3i(chunkX, chunkY, chunkZ); - Chunk chunk = chunkMap.get(chunkPos); - if (chunk == null) return 0; - return chunk.getBlockAt(localX, localY, localZ); - } +// public byte getBlockAt(int x, int y, int z) { +//// int chunkX = x / Chunk.SIZE; +//// int localX = x % Chunk.SIZE; +//// int chunkY = y / Chunk.SIZE; +//// int localY = y % Chunk.SIZE; +//// int chunkZ = z / Chunk.SIZE; +//// int localZ = z % Chunk.SIZE; +//// Vector3i chunkPos = new Vector3i(chunkX, chunkY, chunkZ); +//// Chunk chunk = chunkMap.get(chunkPos); +//// if (chunk == null) return 0; +//// return chunk.getBlockAt(localX, localY, localZ); +// } public Chunk getChunkAt(Vector3i chunkPos) { return chunkMap.get(chunkPos); @@ -70,10 +85,13 @@ public class World { * @return The chunk position. Note that this may not correspond to any existing chunk. */ public static Vector3i getChunkPosAt(Vector3f worldPos) { - return new Vector3i( - (int) Math.floor(worldPos.x / Chunk.SIZE), - (int) Math.floor(worldPos.y / Chunk.SIZE), - (int) Math.floor(worldPos.z / Chunk.SIZE) - ); + return getChunkPosAt(worldPos, new Vector3i()); + } + + public static Vector3i getChunkPosAt(Vector3f worldPos, Vector3i dest) { + dest.x = (int) Math.floor(worldPos.x / Chunk.SIZE); + dest.y = (int) Math.floor(worldPos.y / Chunk.SIZE); + dest.z = (int) Math.floor(worldPos.z / Chunk.SIZE); + return dest; } } diff --git a/core/src/main/java/nl/andrewl/aos_core/net/PlayerJoinMessage.java b/core/src/main/java/nl/andrewl/aos_core/net/PlayerJoinMessage.java new file mode 100644 index 0000000..2ec3ec4 --- /dev/null +++ b/core/src/main/java/nl/andrewl/aos_core/net/PlayerJoinMessage.java @@ -0,0 +1,14 @@ +package nl.andrewl.aos_core.net; + +import nl.andrewl.record_net.Message; + +/** + * An announcement message that's broadcast to all players when a new player + * joins, so that they can add that player to their world. + */ +public record PlayerJoinMessage( + int id, String username, + float px, float py, float pz, + float vx, float vy, float vz, + float ox, float oy +) implements Message {} diff --git a/core/src/main/java/nl/andrewl/aos_core/net/PlayerLeaveMessage.java b/core/src/main/java/nl/andrewl/aos_core/net/PlayerLeaveMessage.java new file mode 100644 index 0000000..26ef0b4 --- /dev/null +++ b/core/src/main/java/nl/andrewl/aos_core/net/PlayerLeaveMessage.java @@ -0,0 +1,10 @@ +package nl.andrewl.aos_core.net; + +import nl.andrewl.record_net.Message; + +/** + * Announcement that's sent when a player leaves, so that all clients can stop + * rendering the player. + */ +public record PlayerLeaveMessage(int id) implements Message { +} diff --git a/core/src/test/java/nl/andrewl/aos_core/model/WorldTest.java b/core/src/test/java/nl/andrewl/aos_core/model/WorldTest.java index f8f7d86..043b9b3 100644 --- a/core/src/test/java/nl/andrewl/aos_core/model/WorldTest.java +++ b/core/src/test/java/nl/andrewl/aos_core/model/WorldTest.java @@ -16,6 +16,7 @@ public class WorldTest { assertEquals(1, world.getBlockAt(new Vector3f(1, 0, 0))); assertEquals(1, world.getBlockAt(new Vector3f(1.9f, 0, 0))); assertEquals(1, world.getBlockAt(new Vector3f(1.5f, 0.7f, 0.3f))); + assertEquals(0, world.getBlockAt(new Vector3f(2f, 0.7f, 0.3f))); } @Test diff --git a/server/src/main/java/nl/andrewl/aos2_server/PlayerManager.java b/server/src/main/java/nl/andrewl/aos2_server/PlayerManager.java index c1cbb46..c93e476 100644 --- a/server/src/main/java/nl/andrewl/aos2_server/PlayerManager.java +++ b/server/src/main/java/nl/andrewl/aos2_server/PlayerManager.java @@ -84,4 +84,18 @@ public class PlayerManager { log.warn("An error occurred while broadcasting a UDP message.", e); } } + + public void broadcastUdpMessageToAllBut(Message msg, ServerPlayer player) { + try { + byte[] data = Net.write(msg); + DatagramPacket packet = new DatagramPacket(data, data.length); + for (var entry : clientHandlers.entrySet()) { + if (entry.getKey() != player.getId()) { + entry.getValue().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 810f111..be44a59 100644 --- a/server/src/main/java/nl/andrewl/aos2_server/Server.java +++ b/server/src/main/java/nl/andrewl/aos2_server/Server.java @@ -1,5 +1,6 @@ package nl.andrewl.aos2_server; +import nl.andrewl.aos_core.model.Chunk; import nl.andrewl.aos_core.model.World; import nl.andrewl.aos_core.model.WorldIO; import nl.andrewl.aos_core.net.UdpReceiver; @@ -8,12 +9,14 @@ 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.joml.Vector3f; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.*; import java.nio.file.Path; +import java.util.Random; import java.util.concurrent.ForkJoinPool; public class Server implements Runnable { @@ -36,26 +39,57 @@ public class Server implements Runnable { this.worldUpdater = new WorldUpdater(this, 20); // Generate world. TODO: do this elsewhere. -// Random rand = new Random(1); -// this.world = new World(); -// for (int x = -5; x <= 5; x++) { -// for (int y = 0; y <= 5; y++) { -// for (int z = -3; z <= 3; z++) { -// Chunk chunk = new Chunk(x, y, z); -// if (y <= 3) { -// for (int i = 0; i < Chunk.TOTAL_SIZE; i++) { -// chunk.getBlocks()[i] = (byte) rand.nextInt(20, 40); -// } -// } -// world.addChunk(chunk); -// } -// } -// } -// world.setBlockAt(new Vector3f(5, 64, 5), (byte) 50); -// world.setBlockAt(new Vector3f(5, 65, 6), (byte) 50); -// world.setBlockAt(new Vector3f(5, 66, 7), (byte) 50); -// WorldIO.write(world, Path.of("testworld")); - this.world = WorldIO.read(Path.of("testworld")); + Random rand = new Random(1); + this.world = new World(); + for (int x = -5; x <= 5; x++) { + for (int y = 0; y <= 5; y++) { + for (int z = -3; z <= 3; z++) { + Chunk chunk = new Chunk(x, y, z); + if (y <= 3) { + for (int i = 0; i < Chunk.TOTAL_SIZE; i++) { + chunk.getBlocks()[i] = (byte) rand.nextInt(20, 40); + } + } + world.addChunk(chunk); + } + } + } + world.setBlockAt(new Vector3f(5, 64, 5), (byte) 50); + world.setBlockAt(new Vector3f(5, 64, 6), (byte) 50); + world.setBlockAt(new Vector3f(5, 64, 7), (byte) 50); + world.setBlockAt(new Vector3f(5, 65, 6), (byte) 50); + world.setBlockAt(new Vector3f(5, 66, 7), (byte) 50); + world.setBlockAt(new Vector3f(5, 65, 7), (byte) 50); + world.setBlockAt(new Vector3f(5, 67, 8), (byte) 50); + world.setBlockAt(new Vector3f(6, 67, 8), (byte) 50); + world.setBlockAt(new Vector3f(7, 67, 8), (byte) 50); + world.setBlockAt(new Vector3f(5, 67, 9), (byte) 50); + world.setBlockAt(new Vector3f(6, 67, 9), (byte) 50); + world.setBlockAt(new Vector3f(7, 67, 9), (byte) 50); + + for (int z = 0; z > -20; z--) { + world.setBlockAt(new Vector3f(0, 63, z), (byte) 120); + } + + for (int x = 0; x < 10; x++) { + world.setBlockAt(new Vector3f(x - 5, 64, 3), (byte) 80); + world.setBlockAt(new Vector3f(x - 5, 65, 3), (byte) 80); + world.setBlockAt(new Vector3f(x - 5, 66, 3), (byte) 80); + } + + for (int z = 0; z < 10; z++) { + world.setBlockAt(new Vector3f(20, 64, z), (byte) 80); + world.setBlockAt(new Vector3f(20, 65, z), (byte) 80); + world.setBlockAt(new Vector3f(20, 66, z), (byte) 80); + } + world.setBlockAt(new Vector3f(21, 64, 6), (byte) 1); + + for (int x = 0; x < 127; x++) { + world.setBlockAt(new Vector3f(x - 50, 63, -15), (byte) x); + } + + WorldIO.write(world, Path.of("testworld")); +// this.world = WorldIO.read(Path.of("testworld")); } @Override @@ -89,7 +123,7 @@ public class Server implements Runnable { ServerPlayer player = playerManager.getPlayer(orientationState.clientId()); if (player != null) { player.setOrientation(orientationState.x(), orientationState.y()); - playerManager.broadcastUdpMessage(new PlayerUpdateMessage(player)); + playerManager.broadcastUdpMessageToAllBut(new PlayerUpdateMessage(player), player); } } } diff --git a/server/src/main/java/nl/andrewl/aos2_server/ServerPlayer.java b/server/src/main/java/nl/andrewl/aos2_server/ServerPlayer.java index 380481b..7f6e96e 100644 --- a/server/src/main/java/nl/andrewl/aos2_server/ServerPlayer.java +++ b/server/src/main/java/nl/andrewl/aos2_server/ServerPlayer.java @@ -1,10 +1,36 @@ package nl.andrewl.aos2_server; import nl.andrewl.aos_core.model.Player; +import nl.andrewl.aos_core.model.World; import nl.andrewl.aos_core.net.udp.ClientInputState; +import org.joml.Math; +import org.joml.Vector2i; +import org.joml.Vector3f; +import org.joml.Vector3fc; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; public class ServerPlayer extends Player { + private static final Logger log = LoggerFactory.getLogger(ServerPlayer.class); + + public static final float HEIGHT = 1.8f; + public static final float HEIGHT_CROUCH = 1.4f; + public static final float WIDTH = 0.75f; + public static final float RADIUS = WIDTH / 2f; + + public static final float GRAVITY = 9.81f * 3; + public static final float SPEED_NORMAL = 4f; + public static final float SPEED_CROUCH = 1.5f; + public static final float SPEED_SPRINT = 9f; + public static final float MOVEMENT_ACCELERATION = 5f; + public static final float MOVEMENT_DECELERATION = 2f; + public static final float JUMP_SPEED = 8f; + private ClientInputState lastInputState; + private boolean updated = false; public ServerPlayer(int id, String username) { super(id, username); @@ -19,4 +45,188 @@ public class ServerPlayer extends Player { public void setLastInputState(ClientInputState inputState) { this.lastInputState = inputState; } + + public boolean isUpdated() { + return updated; + } + + public void tick(float dt, World world) { +// log.info("Ticking player " + id); + updated = false; // Reset the updated flag. This will be set to true if the player was updated in this tick. + + checkBlockCollisions(dt, world); + + if (isGrounded(world)) { +// System.out.println("g"); + tickHorizontalVelocity(); + if (lastInputState.jumping()) velocity.y = JUMP_SPEED; + } else { + velocity.y -= GRAVITY * dt; + updated = true; + } + + // Apply updated velocity to the player. + if (velocity.lengthSquared() > 0) { + Vector3f scaledVelocity = new Vector3f(velocity).mul(dt); + position.add(scaledVelocity); + updated = true; + } + +// System.out.printf("pos: [%.3f, %.3f, %.3f]%n", position.x, position.y, position.z); + } + + private void tickHorizontalVelocity() { + Vector3f horizontalVelocity = new Vector3f( + velocity.x == velocity.x ? velocity.x : 0f, + 0, + velocity.z == velocity.z ? velocity.z : 0f + ); + Vector3f acceleration = new Vector3f(0); + if (lastInputState.forward()) acceleration.z -= 1; + if (lastInputState.backward()) acceleration.z += 1; + if (lastInputState.left()) acceleration.x -= 1; + if (lastInputState.right()) acceleration.x += 1; + if (acceleration.lengthSquared() > 0) { + acceleration.normalize(); + acceleration.rotateAxis(orientation.x, 0, 1, 0); + acceleration.mul(MOVEMENT_ACCELERATION); + horizontalVelocity.add(acceleration); + final float maxSpeed; + if (lastInputState.crouching()) { + maxSpeed = SPEED_CROUCH; + } else if (lastInputState.sprinting()) { + maxSpeed = SPEED_SPRINT; + } else { + maxSpeed = SPEED_NORMAL; + } + if (horizontalVelocity.length() > maxSpeed) { + horizontalVelocity.normalize(maxSpeed); + } + updated = true; + } else if (horizontalVelocity.lengthSquared() > 0) { + Vector3f deceleration = new Vector3f(horizontalVelocity) + .negate().normalize() + .mul(Math.min(horizontalVelocity.length(), MOVEMENT_DECELERATION)); + horizontalVelocity.add(deceleration); + if (horizontalVelocity.length() < 0.1f) { + horizontalVelocity.set(0); + } + updated = true; + } + + // Update the player's velocity with what we've computed. + velocity.x = horizontalVelocity.x; + velocity.z = horizontalVelocity.z; + } + + private boolean isGrounded(World world) { + // Player must be flat on the top of a block. + if (Math.floor(position.y) != position.y) return false; + // Check to see if there's a block under any of the spaces the player is over. + return getHorizontalSpaceOccupied().stream() + .anyMatch(point -> world.getBlockAt(point.x, position.y - 0.1f, point.y) != 0); + } + + private List getHorizontalSpaceOccupied() { + // Get the list of 2d x,z coordinates that we overlap with. + List points = new ArrayList<>(4); // Due to the size of radius, there can only be a max of 4 blocks. + int minX = (int) Math.floor(position.x - RADIUS); + int minZ = (int) Math.floor(position.z - RADIUS); + int maxX = (int) Math.floor(position.x + RADIUS); + int maxZ = (int) Math.floor(position.z + RADIUS); + for (int x = minX; x <= maxX; x++) { + for (int z = minZ; z <= maxZ; z++) { + points.add(new Vector2i(x, z)); + } + } + return points; + } + + private void checkBlockCollisions(float dt, World world) { + final Vector3fc nextTickPosition = new Vector3f(position).add(new Vector3f(velocity).mul(dt)); + List horizontalSpaces = getHorizontalSpaceOccupied(); + int minXNextTick = (int) Math.floor(nextTickPosition.x() - RADIUS); + int minZNextTick = (int) Math.floor(nextTickPosition.z() - RADIUS); + int maxXNextTick = (int) Math.floor(nextTickPosition.x() + RADIUS); + int maxZNextTick = (int) Math.floor(nextTickPosition.z() + RADIUS); + + // Check if the player is about to hit a wall. + // -Z + if ( + world.getBlockAt(nextTickPosition.x(), nextTickPosition.y(), minZNextTick) != 0 && + world.getBlockAt(nextTickPosition.x(), nextTickPosition.y() + 1, minZNextTick) != 0 + ) { + System.out.println("wall -z"); + position.z = ((float) minZNextTick) + RADIUS + 0.001f; + velocity.z = 0; + updated = true; + } + // +Z + if ( + world.getBlockAt(nextTickPosition.x(), nextTickPosition.y(), maxZNextTick) != 0 && + world.getBlockAt(nextTickPosition.x(), nextTickPosition.y() + 1, maxZNextTick) != 0 + ) { + System.out.println("wall +z"); + position.z = ((float) maxZNextTick) - RADIUS - 0.001f; + velocity.z = 0; + updated = true; + } + // -X + if ( + world.getBlockAt(minXNextTick, nextTickPosition.y(), nextTickPosition.z()) != 0 && + world.getBlockAt(minXNextTick, nextTickPosition.y() + 1, nextTickPosition.z()) != 0 + ) { + System.out.println("wall -x"); + position.x = ((float) minXNextTick) + RADIUS + 0.001f; + velocity.x = 0; + updated = true; + } + // +X + if ( + world.getBlockAt(maxXNextTick, nextTickPosition.y(), nextTickPosition.z()) != 0 && + world.getBlockAt(maxXNextTick, nextTickPosition.y() + 1, nextTickPosition.z()) != 0 + ) { + System.out.println("wall +x"); + position.x = ((float) maxXNextTick) - RADIUS - 0.001f; + velocity.x = 0; + updated = true; + } + + // Check if the player is going to hit a ceiling on the next tick, and cancel velocity and set position. + final float nextTickHeadY = nextTickPosition.y() + (lastInputState.crouching() ? HEIGHT_CROUCH : HEIGHT); + boolean playerWillHitCeiling = horizontalSpaces.stream() + .anyMatch(point -> world.getBlockAt(point.x, nextTickHeadY, point.y) != 0); + if (playerWillHitCeiling) { + position.y = Math.floor(nextTickPosition.y()); + if (velocity.y > 0) velocity.y = 0; + updated = true; + } + + // If the player is in the ground, or will be on the next tick, then move them up to the first valid space. + boolean playerFootInBlock = horizontalSpaces.stream() + .anyMatch(point -> world.getBlockAt(point.x, position.y, point.y) != 0 || + world.getBlockAt(point.x, nextTickPosition.y(), point.y) != 0); + if (playerFootInBlock) { +// System.out.println("Player foot in block."); + int nextY = (int) Math.floor(nextTickPosition.y()); + while (true) { +// System.out.println("Checking y = " + nextY); + int finalNextY = nextY; + boolean isOpen = horizontalSpaces.stream() + .allMatch(point -> { +// System.out.printf("[%d, %d, %d] -> %d%n", point.x, finalNextY, point.y, world.getBlockAt(point.x, finalNextY, point.y)); + return world.getBlockAt(point.x, finalNextY, point.y) == 0; + }); + if (isOpen) { +// System.out.println("It's clear to move player to y = " + nextY); + // Move the player to that spot, and cancel out their velocity. + position.y = nextY; + velocity.y = 0; + updated = true; + break; + } + nextY++; + } + } + } } diff --git a/server/src/main/java/nl/andrewl/aos2_server/WorldUpdater.java b/server/src/main/java/nl/andrewl/aos2_server/WorldUpdater.java index d53b38b..a78e114 100644 --- a/server/src/main/java/nl/andrewl/aos2_server/WorldUpdater.java +++ b/server/src/main/java/nl/andrewl/aos2_server/WorldUpdater.java @@ -56,7 +56,8 @@ public class WorldUpdater implements Runnable { private void tick() { for (var player : server.getPlayerManager().getPlayers()) { - updatePlayerMovement(player); + player.tick(secondsPerTick, server.getWorld()); + if (player.isUpdated()) server.getPlayerManager().broadcastUdpMessage(new PlayerUpdateMessage(player)); } } @@ -86,7 +87,7 @@ public class WorldUpdater implements Runnable { boolean grounded = (Math.floor(p.y) == p.y && server.getWorld().getBlockAt(new Vector3f(p.x, p.y - 0.0001f, p.z)) != 0); if (!grounded) { - v.y -= 3f; + v.y -= 9.81f * secondsPerTick; } // Apply horizontal deceleration to the player before computing any input-derived acceleration.