Added more preparations for true multiplayer functionality.
This commit is contained in:
parent
c81da242ef
commit
4ef8e88e81
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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) {}
|
|
@ -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() {
|
||||
//
|
||||
// }
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -14,25 +14,40 @@ import java.util.Map;
|
|||
*/
|
||||
public class World {
|
||||
protected final Map<Vector3ic, Chunk> 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<Vector3ic, Chunk> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {}
|
|
@ -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 {
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue