Added more preparations for true multiplayer functionality.

This commit is contained in:
Andrew Lalis 2022-07-09 16:10:19 +02:00
parent c81da242ef
commit 4ef8e88e81
21 changed files with 562 additions and 80 deletions

View File

@ -89,7 +89,13 @@ public class Camera {
setOrientation((float) Math.toRadians(x), (float) Math.toRadians(y)); 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.identity();
viewTransform.rotate(-orientation.y + ((float) Math.PI / 2), RIGHT); viewTransform.rotate(-orientation.y + ((float) Math.PI / 2), RIGHT);
viewTransform.rotate(-orientation.x, UP); viewTransform.rotate(-orientation.x, UP);

View File

@ -8,7 +8,6 @@ import nl.andrewl.aos_core.model.World;
import nl.andrewl.aos_core.net.ChunkDataMessage; import nl.andrewl.aos_core.net.ChunkDataMessage;
import nl.andrewl.aos_core.net.udp.PlayerUpdateMessage; import nl.andrewl.aos_core.net.udp.PlayerUpdateMessage;
import nl.andrewl.record_net.Message; import nl.andrewl.record_net.Message;
import org.joml.Vector3f;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -27,15 +26,15 @@ public class Client implements Runnable {
private final GameRenderer gameRenderer; private final GameRenderer gameRenderer;
private int clientId; private int clientId;
private final World world; private final ClientWorld world;
public Client(InetAddress serverAddress, int serverPort, String username) { public Client(InetAddress serverAddress, int serverPort, String username) {
this.serverAddress = serverAddress; this.serverAddress = serverAddress;
this.serverPort = serverPort; this.serverPort = serverPort;
this.username = username; this.username = username;
this.communicationHandler = new CommunicationHandler(this); this.communicationHandler = new CommunicationHandler(this);
this.gameRenderer = new GameRenderer(); this.world = new ClientWorld();
this.world = new World(); this.gameRenderer = new GameRenderer(world);
} }
@Override @Override
@ -58,10 +57,8 @@ public class Client implements Runnable {
while (!gameRenderer.windowShouldClose()) { while (!gameRenderer.windowShouldClose()) {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
float dt = (now - lastFrameAt) / 1000f; float dt = (now - lastFrameAt) / 1000f;
gameRenderer.getCamera().interpolatePosition(dt);
gameRenderer.draw(); 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; lastFrameAt = now;
} }
gameRenderer.freeWindow(); gameRenderer.freeWindow();
@ -86,6 +83,7 @@ public class Client implements Runnable {
if (playerUpdate.clientId() == clientId) { if (playerUpdate.clientId() == clientId) {
gameRenderer.getCamera().setPosition(playerUpdate.px(), playerUpdate.py() + 1.8f, playerUpdate.pz()); gameRenderer.getCamera().setPosition(playerUpdate.px(), playerUpdate.py() + 1.8f, playerUpdate.pz());
gameRenderer.getCamera().setVelocity(playerUpdate.vx(), playerUpdate.vy(), playerUpdate.vz()); gameRenderer.getCamera().setVelocity(playerUpdate.vx(), playerUpdate.vy(), playerUpdate.vz());
// TODO: Unload far away chunks and request close chunks we don't have.
} }
} }
} }

View File

@ -5,6 +5,8 @@ import nl.andrewl.aos2_client.CommunicationHandler;
import nl.andrewl.aos_core.net.udp.ClientOrientationState; import nl.andrewl.aos_core.net.udp.ClientOrientationState;
import org.lwjgl.glfw.GLFWCursorPosCallbackI; import org.lwjgl.glfw.GLFWCursorPosCallbackI;
import java.util.concurrent.ForkJoinPool;
import static org.lwjgl.glfw.GLFW.glfwGetCursorPos; import static org.lwjgl.glfw.GLFW.glfwGetCursorPos;
public class PlayerViewCursorCallback implements GLFWCursorPosCallbackI { 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); camera.setOrientation(camera.getOrientation().x - dx * mouseCursorSensitivity, camera.getOrientation().y - dy * mouseCursorSensitivity);
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
if (lastOrientationUpdateSentAt + ORIENTATION_UPDATE_LIMIT < now) { 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; lastOrientationUpdateSentAt = now;
} }
} }

View File

@ -1,6 +1,7 @@
package nl.andrewl.aos2_client.render; package nl.andrewl.aos2_client.render;
import nl.andrewl.aos_core.model.Chunk; import nl.andrewl.aos_core.model.Chunk;
import nl.andrewl.aos_core.model.World;
import static org.lwjgl.opengl.GL46.*; import static org.lwjgl.opengl.GL46.*;
@ -16,9 +17,11 @@ public class ChunkMesh {
private final int[] positionData; private final int[] positionData;
private final Chunk chunk; 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.chunk = chunk;
this.world = world;
this.positionData = new int[]{chunk.getPosition().x, chunk.getPosition().y, chunk.getPosition().z}; this.positionData = new int[]{chunk.getPosition().x, chunk.getPosition().y, chunk.getPosition().z};
this.vboId = glGenBuffers(); this.vboId = glGenBuffers();
@ -39,7 +42,7 @@ public class ChunkMesh {
*/ */
private void loadMesh(ChunkMeshGenerator meshGenerator) { private void loadMesh(ChunkMeshGenerator meshGenerator) {
long start = System.nanoTime(); long start = System.nanoTime();
var meshData = meshGenerator.generateMesh(chunk); var meshData = meshGenerator.generateMesh(chunk, world);
double dur = (System.nanoTime() - start) / 1_000_000.0; double dur = (System.nanoTime() - start) / 1_000_000.0;
this.indexCount = meshData.indexBuffer().limit(); this.indexCount = meshData.indexBuffer().limit();
// Print some debug information. // Print some debug information.

View File

@ -1,6 +1,7 @@
package nl.andrewl.aos2_client.render; package nl.andrewl.aos2_client.render;
import nl.andrewl.aos_core.model.Chunk; import nl.andrewl.aos_core.model.Chunk;
import nl.andrewl.aos_core.model.World;
import org.joml.Vector3f; import org.joml.Vector3f;
import org.joml.Vector3i; import org.joml.Vector3i;
import org.lwjgl.BufferUtils; import org.lwjgl.BufferUtils;
@ -16,16 +17,19 @@ public final class ChunkMeshGenerator {
private final FloatBuffer vertexBuffer; private final FloatBuffer vertexBuffer;
private final IntBuffer indexBuffer; private final IntBuffer indexBuffer;
private final Vector3i pos = new Vector3i(); private final Vector3i pos = new Vector3i();// Pre-allocated vector to hold current local chunk block position.
private final Vector3f color = new Vector3f(); private final Vector3f color = new Vector3f();// Pre-allocated vector to hold current block color.
private final Vector3f norm = new Vector3f(); 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() { public ChunkMeshGenerator() {
vertexBuffer = BufferUtils.createFloatBuffer(300_000); vertexBuffer = BufferUtils.createFloatBuffer(300_000);
indexBuffer = BufferUtils.createIntBuffer(100_000); indexBuffer = BufferUtils.createIntBuffer(100_000);
} }
public ChunkMeshData generateMesh(Chunk chunk) { public ChunkMeshData generateMesh(Chunk chunk, World world) {
vertexBuffer.clear(); vertexBuffer.clear();
indexBuffer.clear(); indexBuffer.clear();
int idx = 0; int idx = 0;
@ -34,12 +38,15 @@ public final class ChunkMeshGenerator {
int x = pos.x; int x = pos.x;
int y = pos.y; int y = pos.y;
int z = pos.z; 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]; byte block = chunk.getBlocks()[i];
if (block <= 0) { if (block <= 0) {
continue; // Don't render empty blocks. 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. // See /design/block_rendering.svg for a diagram of how these vertices are defined.
// var a = new Vector3f(x, y + 1, z + 1); // 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); // var h = new Vector3f(x + 1, y, z);
// Top // 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); norm.set(0, 1, 0);
genFace(idx, genFace(idx,
x, y+1, z+1, // a x, y+1, z+1, // a
@ -63,7 +71,8 @@ public final class ChunkMeshGenerator {
idx += 4; idx += 4;
} }
// Bottom // 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 norm.set(0, -1, 0);// c h g d
genFace(idx, genFace(idx,
x, y, z, // c x, y, z, // c
@ -74,7 +83,8 @@ public final class ChunkMeshGenerator {
idx += 4; idx += 4;
} }
// Positive z // 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); norm.set(0, 0, 1);
genFace(idx, genFace(idx,
x+1, y+1, z+1, // f x+1, y+1, z+1, // f
@ -85,7 +95,8 @@ public final class ChunkMeshGenerator {
idx += 4; idx += 4;
} }
// Negative z // 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); norm.set(0, 0, -1);
genFace(idx, genFace(idx,
x, y+1, z, // b x, y+1, z, // b
@ -96,7 +107,8 @@ public final class ChunkMeshGenerator {
idx += 4; idx += 4;
} }
// Positive x // 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); norm.set(1, 0, 0);
genFace(idx, genFace(idx,
x+1, y+1, z, // e x+1, y+1, z, // e
@ -107,7 +119,8 @@ public final class ChunkMeshGenerator {
idx += 4; idx += 4;
} }
// Negative x // 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); norm.set(-1, 0, 0);
genFace(idx, genFace(idx,
x, y+1, z+1, // a x, y+1, z+1, // a

View File

@ -2,6 +2,7 @@ package nl.andrewl.aos2_client.render;
import nl.andrewl.aos2_client.Camera; import nl.andrewl.aos2_client.Camera;
import nl.andrewl.aos_core.model.Chunk; import nl.andrewl.aos_core.model.Chunk;
import nl.andrewl.aos_core.model.World;
import org.joml.Matrix4f; import org.joml.Matrix4f;
import java.util.ArrayList; import java.util.ArrayList;
@ -50,9 +51,9 @@ public class ChunkRenderer {
glUniformMatrix4fv(projectionTransformUniform, false, projectionTransform.get(new float[16])); glUniformMatrix4fv(projectionTransformUniform, false, projectionTransform.get(new float[16]));
} }
public void draw(Camera cam) { public void draw(Camera cam, World world) {
while (!meshGenerationQueue.isEmpty()) { while (!meshGenerationQueue.isEmpty()) {
chunkMeshes.add(new ChunkMesh(meshGenerationQueue.remove(), chunkMeshGenerator)); chunkMeshes.add(new ChunkMesh(meshGenerationQueue.remove(), world, chunkMeshGenerator));
} }
shaderProgram.use(); shaderProgram.use();
glUniformMatrix4fv(viewTransformUniform, false, cam.getViewTransformData()); glUniformMatrix4fv(viewTransformUniform, false, cam.getViewTransformData());

View File

@ -4,6 +4,7 @@ import nl.andrewl.aos2_client.Camera;
import nl.andrewl.aos2_client.control.PlayerInputKeyCallback; import nl.andrewl.aos2_client.control.PlayerInputKeyCallback;
import nl.andrewl.aos2_client.control.PlayerViewCursorCallback; import nl.andrewl.aos2_client.control.PlayerViewCursorCallback;
import nl.andrewl.aos_core.model.Chunk; import nl.andrewl.aos_core.model.Chunk;
import nl.andrewl.aos_core.model.World;
import org.joml.Matrix4f; import org.joml.Matrix4f;
import org.lwjgl.glfw.Callbacks; import org.lwjgl.glfw.Callbacks;
import org.lwjgl.glfw.GLFWErrorCallback; import org.lwjgl.glfw.GLFWErrorCallback;
@ -28,6 +29,7 @@ public class GameRenderer {
private final ChunkRenderer chunkRenderer; private final ChunkRenderer chunkRenderer;
private final Camera camera; private final Camera camera;
private final World world;
private long windowHandle; private long windowHandle;
private GLFWVidMode primaryMonitorSettings; private GLFWVidMode primaryMonitorSettings;
@ -38,7 +40,8 @@ public class GameRenderer {
private final Matrix4f perspectiveTransform; private final Matrix4f perspectiveTransform;
public GameRenderer() { public GameRenderer(World world) {
this.world = world;
this.chunkRenderer = new ChunkRenderer(); this.chunkRenderer = new ChunkRenderer();
this.camera = new Camera(); this.camera = new Camera();
this.perspectiveTransform = new Matrix4f(); this.perspectiveTransform = new Matrix4f();
@ -140,7 +143,7 @@ public class GameRenderer {
public void draw() { public void draw() {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
chunkRenderer.draw(camera); chunkRenderer.draw(camera, world);
glfwSwapBuffers(windowHandle); glfwSwapBuffers(windowHandle);
glfwPollEvents(); glfwPollEvents();

View File

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

View File

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

View File

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

View File

@ -30,6 +30,8 @@ public final class Net {
serializer.registerType(8, ClientInputState.class); serializer.registerType(8, ClientInputState.class);
serializer.registerType(9, ClientOrientationState.class); serializer.registerType(9, ClientOrientationState.class);
serializer.registerType(10, PlayerUpdateMessage.class); serializer.registerType(10, PlayerUpdateMessage.class);
serializer.registerType(11, PlayerJoinMessage.class);
serializer.registerType(12, PlayerLeaveMessage.class);
} }
public static ExtendedDataInputStream getInputStream(InputStream in) { public static ExtendedDataInputStream getInputStream(InputStream in) {

View File

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

View File

@ -17,12 +17,12 @@ public class Player {
* then the player's y coordinate is y=6.0. The x and z coordinates are * then the player's y coordinate is y=6.0. The x and z coordinates are
* simply the center of the player. * simply the center of the player.
*/ */
private final Vector3f position; protected final Vector3f position;
/** /**
* The player's velocity in each of the coordinate axes. * 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 * 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 * The y component is limited to between 0 and PI, with y=0 looking
* straight down, and y=PI looking straight up. * 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 * A vector that's internally re-computed each time the player's
* orientation changes, and represents unit vector pointing in the * orientation changes, and represents unit vector pointing in the
* direction the player is looking. * direction the player is looking.
*/ */
private final Vector3f viewVector; protected final Vector3f viewVector;
private final String username; protected final String username;
private final int id; protected final int id;
public Player(int id, String username) { public Player(int id, String username) {
this.position = new Vector3f(); this.position = new Vector3f();

View File

@ -14,25 +14,40 @@ import java.util.Map;
*/ */
public class World { public class World {
protected final Map<Vector3ic, Chunk> chunkMap = new HashMap<>(); protected final Map<Vector3ic, Chunk> chunkMap = new HashMap<>();
protected final ColorPalette palette = ColorPalette.rainbow();
public void addChunk(Chunk chunk) { public void addChunk(Chunk chunk) {
chunkMap.put(chunk.getPosition(), chunk); chunkMap.put(chunk.getPosition(), chunk);
} }
public void removeChunk(Vector3i chunkPos) {
chunkMap.remove(chunkPos);
}
public Map<Vector3ic, Chunk> getChunkMap() { public Map<Vector3ic, Chunk> getChunkMap() {
return chunkMap; return chunkMap;
} }
public ColorPalette getPalette() {
return palette;
}
public byte getBlockAt(Vector3f pos) { public byte getBlockAt(Vector3f pos) {
Vector3i chunkPos = getChunkPosAt(pos); return getBlockAt(pos, new Vector3i());
Chunk chunk = chunkMap.get(chunkPos); }
public byte getBlockAt(Vector3f pos, Vector3i util) {
getChunkPosAt(pos, util);
Chunk chunk = chunkMap.get(util);
if (chunk == null) return 0; if (chunk == null) return 0;
Vector3i blockPos = new Vector3i( util.x = (int) Math.floor(pos.x - util.x * Chunk.SIZE);
(int) Math.floor(pos.x - chunkPos.x * Chunk.SIZE), util.y = (int) Math.floor(pos.y - util.y * Chunk.SIZE);
(int) Math.floor(pos.y - chunkPos.y * Chunk.SIZE), util.z = (int) Math.floor(pos.z - util.z * Chunk.SIZE);
(int) Math.floor(pos.z - chunkPos.z * Chunk.SIZE) return chunk.getBlockAt(util);
); }
return chunk.getBlockAt(blockPos);
public byte getBlockAt(float x, float y, float z) {
return getBlockAt(new Vector3f(x, y, z));
} }
public void setBlockAt(Vector3f pos, byte block) { public void setBlockAt(Vector3f pos, byte block) {
@ -47,18 +62,18 @@ public class World {
chunk.setBlockAt(blockPos.x, blockPos.y, blockPos.z, block); chunk.setBlockAt(blockPos.x, blockPos.y, blockPos.z, block);
} }
public byte getBlockAt(int x, int y, int z) { // public byte getBlockAt(int x, int y, int z) {
int chunkX = x / Chunk.SIZE; //// int chunkX = x / Chunk.SIZE;
int localX = x % Chunk.SIZE; //// int localX = x % Chunk.SIZE;
int chunkY = y / Chunk.SIZE; //// int chunkY = y / Chunk.SIZE;
int localY = y % Chunk.SIZE; //// int localY = y % Chunk.SIZE;
int chunkZ = z / Chunk.SIZE; //// int chunkZ = z / Chunk.SIZE;
int localZ = z % Chunk.SIZE; //// int localZ = z % Chunk.SIZE;
Vector3i chunkPos = new Vector3i(chunkX, chunkY, chunkZ); //// Vector3i chunkPos = new Vector3i(chunkX, chunkY, chunkZ);
Chunk chunk = chunkMap.get(chunkPos); //// Chunk chunk = chunkMap.get(chunkPos);
if (chunk == null) return 0; //// if (chunk == null) return 0;
return chunk.getBlockAt(localX, localY, localZ); //// return chunk.getBlockAt(localX, localY, localZ);
} // }
public Chunk getChunkAt(Vector3i chunkPos) { public Chunk getChunkAt(Vector3i chunkPos) {
return chunkMap.get(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. * @return The chunk position. Note that this may not correspond to any existing chunk.
*/ */
public static Vector3i getChunkPosAt(Vector3f worldPos) { public static Vector3i getChunkPosAt(Vector3f worldPos) {
return new Vector3i( return getChunkPosAt(worldPos, new Vector3i());
(int) Math.floor(worldPos.x / Chunk.SIZE), }
(int) Math.floor(worldPos.y / Chunk.SIZE),
(int) Math.floor(worldPos.z / Chunk.SIZE) 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;
} }
} }

View File

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

View File

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

View File

@ -16,6 +16,7 @@ public class WorldTest {
assertEquals(1, world.getBlockAt(new Vector3f(1, 0, 0))); 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.9f, 0, 0)));
assertEquals(1, world.getBlockAt(new Vector3f(1.5f, 0.7f, 0.3f))); assertEquals(1, world.getBlockAt(new Vector3f(1.5f, 0.7f, 0.3f)));
assertEquals(0, world.getBlockAt(new Vector3f(2f, 0.7f, 0.3f)));
} }
@Test @Test

View File

@ -84,4 +84,18 @@ public class PlayerManager {
log.warn("An error occurred while broadcasting a UDP message.", e); 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);
}
}
} }

View File

@ -1,5 +1,6 @@
package nl.andrewl.aos2_server; 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.World;
import nl.andrewl.aos_core.model.WorldIO; import nl.andrewl.aos_core.model.WorldIO;
import nl.andrewl.aos_core.net.UdpReceiver; 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.DatagramInit;
import nl.andrewl.aos_core.net.udp.PlayerUpdateMessage; import nl.andrewl.aos_core.net.udp.PlayerUpdateMessage;
import nl.andrewl.record_net.Message; import nl.andrewl.record_net.Message;
import org.joml.Vector3f;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.net.*; import java.net.*;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Random;
import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ForkJoinPool;
public class Server implements Runnable { public class Server implements Runnable {
@ -36,26 +39,57 @@ public class Server implements Runnable {
this.worldUpdater = new WorldUpdater(this, 20); this.worldUpdater = new WorldUpdater(this, 20);
// Generate world. TODO: do this elsewhere. // Generate world. TODO: do this elsewhere.
// Random rand = new Random(1); Random rand = new Random(1);
// this.world = new World(); this.world = new World();
// for (int x = -5; x <= 5; x++) { for (int x = -5; x <= 5; x++) {
// for (int y = 0; y <= 5; y++) { for (int y = 0; y <= 5; y++) {
// for (int z = -3; z <= 3; z++) { for (int z = -3; z <= 3; z++) {
// Chunk chunk = new Chunk(x, y, z); Chunk chunk = new Chunk(x, y, z);
// if (y <= 3) { if (y <= 3) {
// for (int i = 0; i < Chunk.TOTAL_SIZE; i++) { for (int i = 0; i < Chunk.TOTAL_SIZE; i++) {
// chunk.getBlocks()[i] = (byte) rand.nextInt(20, 40); chunk.getBlocks()[i] = (byte) rand.nextInt(20, 40);
// } }
// } }
// world.addChunk(chunk); world.addChunk(chunk);
// } }
// } }
// } }
// world.setBlockAt(new Vector3f(5, 64, 5), (byte) 50); world.setBlockAt(new Vector3f(5, 64, 5), (byte) 50);
// world.setBlockAt(new Vector3f(5, 65, 6), (byte) 50); world.setBlockAt(new Vector3f(5, 64, 6), (byte) 50);
// world.setBlockAt(new Vector3f(5, 66, 7), (byte) 50); world.setBlockAt(new Vector3f(5, 64, 7), (byte) 50);
// WorldIO.write(world, Path.of("testworld")); world.setBlockAt(new Vector3f(5, 65, 6), (byte) 50);
this.world = WorldIO.read(Path.of("testworld")); 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 @Override
@ -89,7 +123,7 @@ public class Server implements Runnable {
ServerPlayer player = playerManager.getPlayer(orientationState.clientId()); ServerPlayer player = playerManager.getPlayer(orientationState.clientId());
if (player != null) { if (player != null) {
player.setOrientation(orientationState.x(), orientationState.y()); player.setOrientation(orientationState.x(), orientationState.y());
playerManager.broadcastUdpMessage(new PlayerUpdateMessage(player)); playerManager.broadcastUdpMessageToAllBut(new PlayerUpdateMessage(player), player);
} }
} }
} }

View File

@ -1,10 +1,36 @@
package nl.andrewl.aos2_server; package nl.andrewl.aos2_server;
import nl.andrewl.aos_core.model.Player; import nl.andrewl.aos_core.model.Player;
import nl.andrewl.aos_core.model.World;
import nl.andrewl.aos_core.net.udp.ClientInputState; 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 { 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 ClientInputState lastInputState;
private boolean updated = false;
public ServerPlayer(int id, String username) { public ServerPlayer(int id, String username) {
super(id, username); super(id, username);
@ -19,4 +45,188 @@ public class ServerPlayer extends Player {
public void setLastInputState(ClientInputState inputState) { public void setLastInputState(ClientInputState inputState) {
this.lastInputState = 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<Vector2i> getHorizontalSpaceOccupied() {
// Get the list of 2d x,z coordinates that we overlap with.
List<Vector2i> 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<Vector2i> 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++;
}
}
}
} }

View File

@ -56,7 +56,8 @@ public class WorldUpdater implements Runnable {
private void tick() { private void tick() {
for (var player : server.getPlayerManager().getPlayers()) { 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); boolean grounded = (Math.floor(p.y) == p.y && server.getWorld().getBlockAt(new Vector3f(p.x, p.y - 0.0001f, p.z)) != 0);
if (!grounded) { if (!grounded) {
v.y -= 3f; v.y -= 9.81f * secondsPerTick;
} }
// Apply horizontal deceleration to the player before computing any input-derived acceleration. // Apply horizontal deceleration to the player before computing any input-derived acceleration.