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 c1a9bf6..57b06cf 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/Client.java +++ b/client/src/main/java/nl/andrewl/aos2_client/Client.java @@ -1,15 +1,20 @@ package nl.andrewl.aos2_client; +import nl.andrewl.aos2_client.control.InputHandler; import nl.andrewl.aos2_client.control.PlayerInputKeyCallback; +import nl.andrewl.aos2_client.control.PlayerInputMouseClickCallback; import nl.andrewl.aos2_client.control.PlayerViewCursorCallback; import nl.andrewl.aos2_client.render.GameRenderer; import nl.andrewl.aos_core.model.Chunk; import nl.andrewl.aos_core.model.ColorPalette; import nl.andrewl.aos_core.model.World; import nl.andrewl.aos_core.net.ChunkDataMessage; +import nl.andrewl.aos_core.net.ChunkHashMessage; import nl.andrewl.aos_core.net.WorldInfoMessage; +import nl.andrewl.aos_core.net.udp.ChunkUpdateMessage; import nl.andrewl.aos_core.net.udp.PlayerUpdateMessage; import nl.andrewl.record_net.Message; +import org.joml.Vector3i; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,6 +30,7 @@ public class Client implements Runnable { private final String username; private final CommunicationHandler communicationHandler; + private final InputHandler inputHandler; private final GameRenderer gameRenderer; private int clientId; @@ -35,6 +41,7 @@ public class Client implements Runnable { this.serverPort = serverPort; this.username = username; this.communicationHandler = new CommunicationHandler(this); + this.inputHandler = new InputHandler(communicationHandler); this.world = new ClientWorld(); this.gameRenderer = new GameRenderer(world); } @@ -52,7 +59,8 @@ public class Client implements Runnable { gameRenderer.setupWindow( new PlayerViewCursorCallback(gameRenderer.getCamera(), communicationHandler), - new PlayerInputKeyCallback(communicationHandler) + new PlayerInputKeyCallback(inputHandler), + new PlayerInputMouseClickCallback(inputHandler) ); long lastFrameAt = System.currentTimeMillis(); @@ -82,11 +90,22 @@ public class Client implements Runnable { if (msg instanceof ChunkDataMessage chunkDataMessage) { Chunk chunk = chunkDataMessage.toChunk(); world.addChunk(chunk); - gameRenderer.getChunkRenderer().addChunkMesh(chunk); + gameRenderer.getChunkRenderer().queueChunkMesh(chunk); + } + if (msg instanceof ChunkUpdateMessage u) { + Vector3i chunkPos = new Vector3i(u.cx(), u.cy(), u.cz()); + Chunk chunk = world.getChunkAt(chunkPos); + System.out.println(u); + if (chunk != null) { + chunk.setBlockAt(u.lx(), u.ly(), u.lz(), u.newBlock()); + gameRenderer.getChunkRenderer().queueChunkMesh(chunk); + } else { + communicationHandler.sendMessage(new ChunkHashMessage(u.cx(), u.cy(), u.cz(), -1)); + } } if (msg instanceof PlayerUpdateMessage playerUpdate) { if (playerUpdate.clientId() == clientId) { - float eyeHeight = playerUpdate.crouching() ? 1.1f : 1.7f; + float eyeHeight = playerUpdate.crouching() ? 1.3f : 1.7f; gameRenderer.getCamera().setPosition(playerUpdate.px(), playerUpdate.py() + eyeHeight, 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/InputHandler.java b/client/src/main/java/nl/andrewl/aos2_client/control/InputHandler.java new file mode 100644 index 0000000..91cda77 --- /dev/null +++ b/client/src/main/java/nl/andrewl/aos2_client/control/InputHandler.java @@ -0,0 +1,39 @@ +package nl.andrewl.aos2_client.control; + +import nl.andrewl.aos2_client.CommunicationHandler; +import nl.andrewl.aos_core.net.udp.ClientInputState; + +import static org.lwjgl.glfw.GLFW.*; + +/** + * Class which manages the player's input, and sending it to the server. + */ +public class InputHandler { + private final CommunicationHandler comm; + + private ClientInputState lastInputState = null; + + public InputHandler(CommunicationHandler comm) { + this.comm = comm; + } + + public void updateInputState(long window) { + // TODO: Allow customized keybindings. + ClientInputState currentInputState = new ClientInputState( + comm.getClientId(), + glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS, + glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS, + glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS, + glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS, + glfwGetKey(window, GLFW_KEY_SPACE) == GLFW_PRESS, + glfwGetKey(window, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS, + glfwGetKey(window, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS, + glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_1) == GLFW_PRESS, + glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_2) == GLFW_PRESS + ); + if (!currentInputState.equals(lastInputState)) { + comm.sendDatagramPacket(currentInputState); + lastInputState = currentInputState; + } + } +} diff --git a/client/src/main/java/nl/andrewl/aos2_client/control/PlayerInputKeyCallback.java b/client/src/main/java/nl/andrewl/aos2_client/control/PlayerInputKeyCallback.java index 9e74c94..c088a67 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/control/PlayerInputKeyCallback.java +++ b/client/src/main/java/nl/andrewl/aos2_client/control/PlayerInputKeyCallback.java @@ -1,17 +1,14 @@ package nl.andrewl.aos2_client.control; -import nl.andrewl.aos2_client.CommunicationHandler; -import nl.andrewl.aos_core.net.udp.ClientInputState; import org.lwjgl.glfw.GLFWKeyCallbackI; import static org.lwjgl.glfw.GLFW.*; public class PlayerInputKeyCallback implements GLFWKeyCallbackI { - private ClientInputState lastInputState = null; - private final CommunicationHandler comm; + private final InputHandler inputHandler; - public PlayerInputKeyCallback(CommunicationHandler comm) { - this.comm = comm; + public PlayerInputKeyCallback(InputHandler inputHandler) { + this.inputHandler = inputHandler; } @Override @@ -19,20 +16,6 @@ public class PlayerInputKeyCallback implements GLFWKeyCallbackI { if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE) { glfwSetWindowShouldClose(window, true); } - - ClientInputState inputState = new ClientInputState( - comm.getClientId(), - glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS, - glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS, - glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS, - glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS, - glfwGetKey(window, GLFW_KEY_SPACE) == GLFW_PRESS, - glfwGetKey(window, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS, - glfwGetKey(window, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS - ); - if (!inputState.equals(lastInputState)) { - comm.sendDatagramPacket(inputState); - lastInputState = inputState; - } + inputHandler.updateInputState(window); } } diff --git a/client/src/main/java/nl/andrewl/aos2_client/control/PlayerInputMouseClickCallback.java b/client/src/main/java/nl/andrewl/aos2_client/control/PlayerInputMouseClickCallback.java new file mode 100644 index 0000000..b4c63fe --- /dev/null +++ b/client/src/main/java/nl/andrewl/aos2_client/control/PlayerInputMouseClickCallback.java @@ -0,0 +1,20 @@ +package nl.andrewl.aos2_client.control; + +import org.lwjgl.glfw.GLFWMouseButtonCallbackI; + +/** + * Callback that's called when the player clicks with their mouse. + */ +public class PlayerInputMouseClickCallback implements GLFWMouseButtonCallbackI { + private final InputHandler inputHandler; + + public PlayerInputMouseClickCallback(InputHandler inputHandler) { + this.inputHandler = inputHandler; + } + + @Override + public void invoke(long window, int button, int action, int mods) { + System.out.println("Click: " + button); + inputHandler.updateInputState(window); + } +} 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 7ee4afe..579c0da 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 @@ -9,6 +9,10 @@ import java.util.concurrent.ForkJoinPool; import static org.lwjgl.glfw.GLFW.glfwGetCursorPos; +/** + * Callback that's called when the player's cursor position updates. This means + * the player is looking around. + */ public class PlayerViewCursorCallback implements GLFWCursorPosCallbackI { /** * The number of milliseconds to wait before sending orientation updates, 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 e592d8b..9ddca49 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 @@ -2,6 +2,8 @@ package nl.andrewl.aos2_client.render; import nl.andrewl.aos_core.model.Chunk; import nl.andrewl.aos_core.model.World; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import static org.lwjgl.opengl.GL46.*; @@ -9,6 +11,8 @@ import static org.lwjgl.opengl.GL46.*; * Represents a 3d mesh for a chunk. */ public class ChunkMesh { + private static final Logger log = LoggerFactory.getLogger(ChunkMesh.class); + private final int vboId; private final int vaoId; private final int eboId; @@ -50,12 +54,11 @@ public class ChunkMesh { double dur = (System.nanoTime() - start) / 1_000_000.0; this.indexCount = meshData.indexBuffer().limit(); // Print some debug information. - System.out.printf( - "Generated mesh for chunk (%d, %d, %d) in %.3f ms. %d vertices, %d indices.%n", + log.debug( + "Generated mesh for chunk ({}, {}, {}) in {} ms. {} vertices and {} indices.", chunk.getPosition().x, chunk.getPosition().y, chunk.getPosition().z, dur, - meshData.vertexBuffer().limit() / 9, - indexCount + meshData.vertexBuffer().limit() / 9, indexCount ); glBindBuffer(GL_ARRAY_BUFFER, vboId); 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 0f527f3..0ed70dc 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 @@ -42,7 +42,7 @@ public class ChunkRenderer { glUniform1i(chunkSizeUniform, Chunk.SIZE); } - public void addChunkMesh(Chunk chunk) { + public void queueChunkMesh(Chunk chunk) { meshGenerationQueue.add(chunk); } @@ -54,6 +54,8 @@ public class ChunkRenderer { while (!meshGenerationQueue.isEmpty()) { Chunk chunk = meshGenerationQueue.remove(); ChunkMesh mesh = new ChunkMesh(chunk, world, chunkMeshGenerator); + ChunkMesh existingMesh = chunkMeshes.get(chunk.getPosition()); + if (existingMesh != null) existingMesh.free(); chunkMeshes.put(chunk.getPosition(), mesh); } shaderProgram.use(); 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 7033b07..8924b61 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 @@ -1,14 +1,9 @@ package nl.andrewl.aos2_client.render; 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; -import org.lwjgl.glfw.GLFWVidMode; +import org.lwjgl.glfw.*; import org.lwjgl.opengl.GL; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,7 +43,7 @@ public class GameRenderer { } - public void setupWindow(PlayerViewCursorCallback viewCursorCallback, PlayerInputKeyCallback inputKeyCallback) { + public void setupWindow(GLFWCursorPosCallbackI viewCursorCallback, GLFWKeyCallbackI inputKeyCallback, GLFWMouseButtonCallbackI mouseButtonCallback) { GLFWErrorCallback.createPrint(System.err).set(); if (!glfwInit()) throw new IllegalStateException("Could not initialize GLFW."); glfwDefaultWindowHints(); @@ -65,6 +60,7 @@ public class GameRenderer { // Setup callbacks. glfwSetKeyCallback(windowHandle, inputKeyCallback); glfwSetCursorPosCallback(windowHandle, viewCursorCallback); + glfwSetMouseButtonCallback(windowHandle, mouseButtonCallback); glfwSetInputMode(windowHandle, GLFW_CURSOR, GLFW_CURSOR_DISABLED); glfwSetInputMode(windowHandle, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE); glfwSetCursorPos(windowHandle, 0, 0); @@ -118,13 +114,16 @@ public class GameRenderer { updatePerspective(); } + public float getAspectRatio() { + return (float) screenWidth / (float) screenHeight; + } + /** * Updates the rendering perspective used to render the game. Note: only * call this after calling {@link ChunkRenderer#setupShaderProgram()}. */ private void updatePerspective() { - float aspect = (float) screenWidth / (float) screenHeight; - perspectiveTransform.setPerspective(fov, aspect, Z_NEAR, Z_FAR); + perspectiveTransform.setPerspective(fov, getAspectRatio(), Z_NEAR, Z_FAR); chunkRenderer.setPerspective(perspectiveTransform); } diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/font/Character.java b/client/src/main/java/nl/andrewl/aos2_client/render/font/Character.java new file mode 100644 index 0000000..9dd4951 --- /dev/null +++ b/client/src/main/java/nl/andrewl/aos2_client/render/font/Character.java @@ -0,0 +1,104 @@ +package nl.andrewl.aos2_client.render.font; + +/** + * Simple data structure class holding information about a certain glyph in the + * font texture atlas. All sizes are for a font-size of 1. + * + * @author Karl + * + */ +public class Character { + + private int id; + private double xTextureCoord; + private double yTextureCoord; + private double xMaxTextureCoord; + private double yMaxTextureCoord; + private double xOffset; + private double yOffset; + private double sizeX; + private double sizeY; + private double xAdvance; + + /** + * @param id + * - the ASCII value of the character. + * @param xTextureCoord + * - the x texture coordinate for the top left corner of the + * character in the texture atlas. + * @param yTextureCoord + * - the y texture coordinate for the top left corner of the + * character in the texture atlas. + * @param xTexSize + * - the width of the character in the texture atlas. + * @param yTexSize + * - the height of the character in the texture atlas. + * @param xOffset + * - the x distance from the curser to the left edge of the + * character's quad. + * @param yOffset + * - the y distance from the curser to the top edge of the + * character's quad. + * @param sizeX + * - the width of the character's quad in screen space. + * @param sizeY + * - the height of the character's quad in screen space. + * @param xAdvance + * - how far in pixels the cursor should advance after adding + * this character. + */ + protected Character(int id, double xTextureCoord, double yTextureCoord, double xTexSize, double yTexSize, + double xOffset, double yOffset, double sizeX, double sizeY, double xAdvance) { + this.id = id; + this.xTextureCoord = xTextureCoord; + this.yTextureCoord = yTextureCoord; + this.xOffset = xOffset; + this.yOffset = yOffset; + this.sizeX = sizeX; + this.sizeY = sizeY; + this.xMaxTextureCoord = xTexSize + xTextureCoord; + this.yMaxTextureCoord = yTexSize + yTextureCoord; + this.xAdvance = xAdvance; + } + + protected int getId() { + return id; + } + + protected double getxTextureCoord() { + return xTextureCoord; + } + + protected double getyTextureCoord() { + return yTextureCoord; + } + + protected double getXMaxTextureCoord() { + return xMaxTextureCoord; + } + + protected double getYMaxTextureCoord() { + return yMaxTextureCoord; + } + + protected double getxOffset() { + return xOffset; + } + + protected double getyOffset() { + return yOffset; + } + + protected double getSizeX() { + return sizeX; + } + + protected double getSizeY() { + return sizeY; + } + + protected double getxAdvance() { + return xAdvance; + } + +} diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/font/FontRenderer.java b/client/src/main/java/nl/andrewl/aos2_client/render/font/FontRenderer.java new file mode 100644 index 0000000..b0d32bf --- /dev/null +++ b/client/src/main/java/nl/andrewl/aos2_client/render/font/FontRenderer.java @@ -0,0 +1,20 @@ +package nl.andrewl.aos2_client.render.font; + +import nl.andrewl.aos2_client.render.ShaderProgram; + +import static org.lwjgl.opengl.GL46.*; + +public class FontRenderer { + private final ShaderProgram shaderProgram; + + public FontRenderer() { + shaderProgram = new ShaderProgram.Builder() + .withShader("/shader/text/vertex.glsl", GL_VERTEX_SHADER) + .withShader("/shader/text/fragment.glsl", GL_FRAGMENT_SHADER) + .build(); + } + + public void free() { + shaderProgram.free(); + } +} diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/font/FontType.java b/client/src/main/java/nl/andrewl/aos2_client/render/font/FontType.java new file mode 100644 index 0000000..bc4303d --- /dev/null +++ b/client/src/main/java/nl/andrewl/aos2_client/render/font/FontType.java @@ -0,0 +1,52 @@ +package nl.andrewl.aos2_client.render.font; + +import java.io.File; + +/** + * Represents a font. It holds the font's texture atlas as well as having the + * ability to create the quad vertices for any text using this font. + * + * @author Karl + * + */ +public class FontType { + + private final int textureAtlas; + private final TextMeshCreator loader; + + /** + * Creates a new font and loads up the data about each character from the + * font file. + * + * @param textureAtlas + * - the ID of the font atlas texture. + * @param fontFile + * - the font file containing information about each character in + * the texture atlas. + */ + public FontType(int textureAtlas, File fontFile, float aspectRatio) { + this.textureAtlas = textureAtlas; + this.loader = new TextMeshCreator(fontFile, aspectRatio); + } + + /** + * @return The font texture atlas. + */ + public int getTextureAtlas() { + return textureAtlas; + } + + /** + * Takes in an unloaded text and calculate all of the vertices for the quads + * on which this text will be rendered. The vertex positions and texture + * coords and calculated based on the information from the font file. + * + * @param text + * - the unloaded text. + * @return Information about the vertices of all the quads. + */ + public TextMeshData loadText(GUIText text) { + return loader.createTextMesh(text); + } + +} diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/font/GUIText.java b/client/src/main/java/nl/andrewl/aos2_client/render/font/GUIText.java new file mode 100644 index 0000000..fc8b1a3 --- /dev/null +++ b/client/src/main/java/nl/andrewl/aos2_client/render/font/GUIText.java @@ -0,0 +1,196 @@ +package nl.andrewl.aos2_client.render.font; + +import org.joml.Vector2f; +import org.joml.Vector3f; +import org.lwjgl.BufferUtils; + +import java.nio.FloatBuffer; + +import static org.lwjgl.opengl.GL46.*; + +/** + * Represents a piece of text in the game. + * + * @author Karl + * + */ +public class GUIText { + + private final String textString; + private final float fontSize; + + private int textMeshVao; + private int textMeshVbo; + private int vertexCount; + private final Vector3f colour = new Vector3f(0f, 0f, 0f); + + private final Vector2f position; + private final float lineMaxSize; + private int numberOfLines; + + private final FontType font; + + private boolean centerText; + + /** + * Creates a new text, loads the text's quads into a VAO, and adds the text + * to the screen. + * + * @param text + * - the text. + * @param fontSize + * - the font size of the text, where a font size of 1 is the + * default size. + * @param font + * - the font that this text should use. + * @param position + * - the position on the screen where the top left corner of the + * text should be rendered. The top left corner of the screen is + * (0, 0) and the bottom right is (1, 1). + * @param maxLineLength + * - basically the width of the virtual page in terms of screen + * width (1 is full screen width, 0.5 is half the width of the + * screen, etc.) Text cannot go off the edge of the page, so if + * the text is longer than this length it will go onto the next + * line. When text is centered it is centered into the middle of + * the line, based on this line length value. + * @param centered + * - whether the text should be centered or not. + */ + public GUIText(String text, float fontSize, FontType font, Vector2f position, float maxLineLength, + boolean centered) { + this.textString = text; + this.fontSize = fontSize; + this.font = font; + this.position = position; + this.lineMaxSize = maxLineLength; + this.centerText = centered; + // load text + TextMeshData meshData = font.loadText(this); + textMeshVao = glGenVertexArrays(); + textMeshVbo = glGenBuffers(); + glBindBuffer(GL_ARRAY_BUFFER, textMeshVbo); +// FloatBuffer buffer1 = BufferUtils.createFloatBuffer(meshData) + } + + /** + * Remove the text from the screen. + */ + public void remove() { + // remove text + } + + /** + * @return The font used by this text. + */ + public FontType getFont() { + return font; + } + + /** + * Set the colour of the text. + * + * @param r + * - red value, between 0 and 1. + * @param g + * - green value, between 0 and 1. + * @param b + * - blue value, between 0 and 1. + */ + public void setColour(float r, float g, float b) { + colour.set(r, g, b); + } + + /** + * @return the colour of the text. + */ + public Vector3f getColour() { + return colour; + } + + /** + * @return The number of lines of text. This is determined when the text is + * loaded, based on the length of the text and the max line length + * that is set. + */ + public int getNumberOfLines() { + return numberOfLines; + } + + /** + * @return The position of the top-left corner of the text in screen-space. + * (0, 0) is the top left corner of the screen, (1, 1) is the bottom + * right. + */ + public Vector2f getPosition() { + return position; + } + + /** + * @return the ID of the text's VAO, which contains all the vertex data for + * the quads on which the text will be rendered. + */ + public int getMesh() { + return textMeshVao; + } + + /** + * Set the VAO and vertex count for this text. + * + * @param vao + * - the VAO containing all the vertex data for the quads on + * which the text will be rendered. + * @param verticesCount + * - the total number of vertices in all of the quads. + */ + public void setMeshInfo(int vao, int verticesCount) { + this.textMeshVao = vao; + this.vertexCount = verticesCount; + } + + /** + * @return The total number of vertices of all the text's quads. + */ + public int getVertexCount() { + return this.vertexCount; + } + + /** + * @return the font size of the text (a font size of 1 is normal). + */ + protected float getFontSize() { + return fontSize; + } + + /** + * Sets the number of lines that this text covers (method used only in + * loading). + * + * @param number + */ + protected void setNumberOfLines(int number) { + this.numberOfLines = number; + } + + /** + * @return {@code true} if the text should be centered. + */ + protected boolean isCentered() { + return centerText; + } + + /** + * @return The maximum length of a line of this text. + */ + protected float getMaxLineSize() { + return lineMaxSize; + } + + /** + * @return The string of text. + */ + protected String getTextString() { + return textString; + } + +} diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/font/Line.java b/client/src/main/java/nl/andrewl/aos2_client/render/font/Line.java new file mode 100644 index 0000000..c188d5b --- /dev/null +++ b/client/src/main/java/nl/andrewl/aos2_client/render/font/Line.java @@ -0,0 +1,77 @@ +package nl.andrewl.aos2_client.render.font; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a line of text during the loading of a text. + * + * @author Karl + * + */ +public class Line { + + private final double maxLength; + private final double spaceSize; + + private final List words = new ArrayList(); + private double currentLineLength = 0; + + /** + * Creates an empty line. + * + * @param spaceWidth + * - the screen-space width of a space character. + * @param fontSize + * - the size of font being used. + * @param maxLength + * - the screen-space maximum length of a line. + */ + protected Line(double spaceWidth, double fontSize, double maxLength) { + this.spaceSize = spaceWidth * fontSize; + this.maxLength = maxLength; + } + + /** + * Attempt to add a word to the line. If the line can fit the word in + * without reaching the maximum line length then the word is added and the + * line length increased. + * + * @param word + * - the word to try to add. + * @return {@code true} if the word has successfully been added to the line. + */ + protected boolean attemptToAddWord(Word word) { + double additionalLength = word.getWordWidth(); + additionalLength += !words.isEmpty() ? spaceSize : 0; + if (currentLineLength + additionalLength <= maxLength) { + words.add(word); + currentLineLength += additionalLength; + return true; + } else { + return false; + } + } + + /** + * @return The max length of the line. + */ + protected double getMaxLength() { + return maxLength; + } + + /** + * @return The current screen-space length of the line. + */ + protected double getLineLength() { + return currentLineLength; + } + + /** + * @return The list of words in the line. + */ + protected List getWords() { + return words; + } + +} diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/font/MetaFile.java b/client/src/main/java/nl/andrewl/aos2_client/render/font/MetaFile.java new file mode 100644 index 0000000..0cee884 --- /dev/null +++ b/client/src/main/java/nl/andrewl/aos2_client/render/font/MetaFile.java @@ -0,0 +1,213 @@ +package nl.andrewl.aos2_client.render.font; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * Provides functionality for getting the values from a font file. + * + * @author Karl + * + */ +public class MetaFile { + + private static final int PAD_TOP = 0; + private static final int PAD_LEFT = 1; + private static final int PAD_BOTTOM = 2; + private static final int PAD_RIGHT = 3; + + private static final int DESIRED_PADDING = 3; + + private static final String SPLITTER = " "; + private static final String NUMBER_SEPARATOR = ","; + + private final double aspectRatio; + + private double verticalPerPixelSize; + private double horizontalPerPixelSize; + private double spaceWidth; + private int[] padding; + private int paddingWidth; + private int paddingHeight; + + private final Map metaData = new HashMap<>(); + + private BufferedReader reader; + private final Map values = new HashMap<>(); + + /** + * Opens a font file in preparation for reading. + * + * @param file + * - the font file. + */ + protected MetaFile(File file, float aspectRatio) { + this.aspectRatio = aspectRatio; + openFile(file); + loadPaddingData(); + loadLineSizes(); + int imageWidth = getValueOfVariable("scaleW"); + loadCharacterData(imageWidth); + close(); + } + + protected double getSpaceWidth() { + return spaceWidth; + } + + protected Character getCharacter(int ascii) { + return metaData.get(ascii); + } + + /** + * Read in the next line and store the variable values. + * + * @return {@code true} if the end of the file hasn't been reached. + */ + private boolean processNextLine() { + values.clear(); + String line = null; + try { + line = reader.readLine(); + } catch (IOException e1) { + } + if (line == null) { + return false; + } + for (String part : line.split(SPLITTER)) { + String[] valuePairs = part.split("="); + if (valuePairs.length == 2) { + values.put(valuePairs[0], valuePairs[1]); + } + } + return true; + } + + /** + * Gets the {@code int} value of the variable with a certain name on the + * current line. + * + * @param variable + * - the name of the variable. + * @return The value of the variable. + */ + private int getValueOfVariable(String variable) { + return Integer.parseInt(values.get(variable)); + } + + /** + * Gets the array of ints associated with a variable on the current line. + * + * @param variable + * - the name of the variable. + * @return The int array of values associated with the variable. + */ + private int[] getValuesOfVariable(String variable) { + String[] numbers = values.get(variable).split(NUMBER_SEPARATOR); + int[] actualValues = new int[numbers.length]; + for (int i = 0; i < actualValues.length; i++) { + actualValues[i] = Integer.parseInt(numbers[i]); + } + return actualValues; + } + + /** + * Closes the font file after finishing reading. + */ + private void close() { + try { + reader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * Opens the font file, ready for reading. + * + * @param file + * - the font file. + */ + private void openFile(File file) { + try { + reader = new BufferedReader(new FileReader(file)); + } catch (Exception e) { + e.printStackTrace(); + System.err.println("Couldn't read font meta file!"); + } + } + + /** + * Loads the data about how much padding is used around each character in + * the texture atlas. + */ + private void loadPaddingData() { + processNextLine(); + this.padding = getValuesOfVariable("padding"); + this.paddingWidth = padding[PAD_LEFT] + padding[PAD_RIGHT]; + this.paddingHeight = padding[PAD_TOP] + padding[PAD_BOTTOM]; + } + + /** + * Loads information about the line height for this font in pixels, and uses + * this as a way to find the conversion rate between pixels in the texture + * atlas and screen-space. + */ + private void loadLineSizes() { + processNextLine(); + int lineHeightPixels = getValueOfVariable("lineHeight") - paddingHeight; + verticalPerPixelSize = TextMeshCreator.LINE_HEIGHT / (double) lineHeightPixels; + horizontalPerPixelSize = verticalPerPixelSize / aspectRatio; + } + + /** + * Loads in data about each character and stores the data in the + * {@link Character} class. + * + * @param imageWidth + * - the width of the texture atlas in pixels. + */ + private void loadCharacterData(int imageWidth) { + processNextLine(); + processNextLine(); + while (processNextLine()) { + Character c = loadCharacter(imageWidth); + if (c != null) { + metaData.put(c.getId(), c); + } + } + } + + /** + * Loads all the data about one character in the texture atlas and converts + * it all from 'pixels' to 'screen-space' before storing. The effects of + * padding are also removed from the data. + * + * @param imageSize + * - the size of the texture atlas in pixels. + * @return The data about the character. + */ + private Character loadCharacter(int imageSize) { + int id = getValueOfVariable("id"); + if (id == TextMeshCreator.SPACE_ASCII) { + this.spaceWidth = (getValueOfVariable("xadvance") - paddingWidth) * horizontalPerPixelSize; + return null; + } + double xTex = ((double) getValueOfVariable("x") + (padding[PAD_LEFT] - DESIRED_PADDING)) / imageSize; + double yTex = ((double) getValueOfVariable("y") + (padding[PAD_TOP] - DESIRED_PADDING)) / imageSize; + int width = getValueOfVariable("width") - (paddingWidth - (2 * DESIRED_PADDING)); + int height = getValueOfVariable("height") - ((paddingHeight) - (2 * DESIRED_PADDING)); + double quadWidth = width * horizontalPerPixelSize; + double quadHeight = height * verticalPerPixelSize; + double xTexSize = (double) width / imageSize; + double yTexSize = (double) height / imageSize; + double xOff = (getValueOfVariable("xoffset") + padding[PAD_LEFT] - DESIRED_PADDING) * horizontalPerPixelSize; + double yOff = (getValueOfVariable("yoffset") + (padding[PAD_TOP] - DESIRED_PADDING)) * verticalPerPixelSize; + double xAdvance = (getValueOfVariable("xadvance") - paddingWidth) * horizontalPerPixelSize; + return new Character(id, xTex, yTex, xTexSize, yTexSize, xOff, yOff, quadWidth, quadHeight, xAdvance); + } +} diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/font/TextMeshCreator.java b/client/src/main/java/nl/andrewl/aos2_client/render/font/TextMeshCreator.java new file mode 100644 index 0000000..342b1cc --- /dev/null +++ b/client/src/main/java/nl/andrewl/aos2_client/render/font/TextMeshCreator.java @@ -0,0 +1,133 @@ +package nl.andrewl.aos2_client.render.font; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +public class TextMeshCreator { + + protected static final double LINE_HEIGHT = 0.03f; + protected static final int SPACE_ASCII = 32; + + private final MetaFile metaData; + + protected TextMeshCreator(File metaFile, float aspectRatio) { + metaData = new MetaFile(metaFile, aspectRatio); + } + + protected TextMeshData createTextMesh(GUIText text) { + List lines = createStructure(text); + return createQuadVertices(text, lines); + } + + private List createStructure(GUIText text) { + char[] chars = text.getTextString().toCharArray(); + List lines = new ArrayList(); + Line currentLine = new Line(metaData.getSpaceWidth(), text.getFontSize(), text.getMaxLineSize()); + Word currentWord = new Word(text.getFontSize()); + for (char c : chars) { + if ((int) c == SPACE_ASCII) { + boolean added = currentLine.attemptToAddWord(currentWord); + if (!added) { + lines.add(currentLine); + currentLine = new Line(metaData.getSpaceWidth(), text.getFontSize(), text.getMaxLineSize()); + currentLine.attemptToAddWord(currentWord); + } + currentWord = new Word(text.getFontSize()); + continue; + } + Character character = metaData.getCharacter(c); + currentWord.addCharacter(character); + } + completeStructure(lines, currentLine, currentWord, text); + return lines; + } + + private void completeStructure(List lines, Line currentLine, Word currentWord, GUIText text) { + boolean added = currentLine.attemptToAddWord(currentWord); + if (!added) { + lines.add(currentLine); + currentLine = new Line(metaData.getSpaceWidth(), text.getFontSize(), text.getMaxLineSize()); + currentLine.attemptToAddWord(currentWord); + } + lines.add(currentLine); + } + + private TextMeshData createQuadVertices(GUIText text, List lines) { + text.setNumberOfLines(lines.size()); + double curserX = 0f; + double curserY = 0f; + List vertices = new ArrayList(); + List textureCoords = new ArrayList(); + for (Line line : lines) { + if (text.isCentered()) { + curserX = (line.getMaxLength() - line.getLineLength()) / 2; + } + for (Word word : line.getWords()) { + for (Character letter : word.getCharacters()) { + addVerticesForCharacter(curserX, curserY, letter, text.getFontSize(), vertices); + addTexCoords(textureCoords, letter.getxTextureCoord(), letter.getyTextureCoord(), + letter.getXMaxTextureCoord(), letter.getYMaxTextureCoord()); + curserX += letter.getxAdvance() * text.getFontSize(); + } + curserX += metaData.getSpaceWidth() * text.getFontSize(); + } + curserX = 0; + curserY += LINE_HEIGHT * text.getFontSize(); + } + return new TextMeshData(listToArray(vertices), listToArray(textureCoords)); + } + + private void addVerticesForCharacter(double curserX, double curserY, Character character, double fontSize, + List vertices) { + double x = curserX + (character.getxOffset() * fontSize); + double y = curserY + (character.getyOffset() * fontSize); + double maxX = x + (character.getSizeX() * fontSize); + double maxY = y + (character.getSizeY() * fontSize); + double properX = (2 * x) - 1; + double properY = (-2 * y) + 1; + double properMaxX = (2 * maxX) - 1; + double properMaxY = (-2 * maxY) + 1; + addVertices(vertices, properX, properY, properMaxX, properMaxY); + } + + private static void addVertices(List vertices, double x, double y, double maxX, double maxY) { + vertices.add((float) x); + vertices.add((float) y); + vertices.add((float) x); + vertices.add((float) maxY); + vertices.add((float) maxX); + vertices.add((float) maxY); + vertices.add((float) maxX); + vertices.add((float) maxY); + vertices.add((float) maxX); + vertices.add((float) y); + vertices.add((float) x); + vertices.add((float) y); + } + + private static void addTexCoords(List texCoords, double x, double y, double maxX, double maxY) { + texCoords.add((float) x); + texCoords.add((float) y); + texCoords.add((float) x); + texCoords.add((float) maxY); + texCoords.add((float) maxX); + texCoords.add((float) maxY); + texCoords.add((float) maxX); + texCoords.add((float) maxY); + texCoords.add((float) maxX); + texCoords.add((float) y); + texCoords.add((float) x); + texCoords.add((float) y); + } + + + private static float[] listToArray(List listOfFloats) { + float[] array = new float[listOfFloats.size()]; + for (int i = 0; i < array.length; i++) { + array[i] = listOfFloats.get(i); + } + return array; + } + +} diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/font/TextMeshData.java b/client/src/main/java/nl/andrewl/aos2_client/render/font/TextMeshData.java new file mode 100644 index 0000000..b3149d4 --- /dev/null +++ b/client/src/main/java/nl/andrewl/aos2_client/render/font/TextMeshData.java @@ -0,0 +1,30 @@ +package nl.andrewl.aos2_client.render.font; + +/** + * Stores the vertex data for all the quads on which a text will be rendered. + * @author Karl + * + */ +public class TextMeshData { + + private final float[] vertexPositions; + private final float[] textureCoords; + + protected TextMeshData(float[] vertexPositions, float[] textureCoords){ + this.vertexPositions = vertexPositions; + this.textureCoords = textureCoords; + } + + public float[] getVertexPositions() { + return vertexPositions; + } + + public float[] getTextureCoords() { + return textureCoords; + } + + public int getVertexCount() { + return vertexPositions.length/2; + } + +} diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/font/Word.java b/client/src/main/java/nl/andrewl/aos2_client/render/font/Word.java new file mode 100644 index 0000000..254c4b3 --- /dev/null +++ b/client/src/main/java/nl/andrewl/aos2_client/render/font/Word.java @@ -0,0 +1,48 @@ +package nl.andrewl.aos2_client.render.font; + +import java.util.ArrayList; +import java.util.List; + +/** + * During the loading of a text this represents one word in the text. + * @author Karl + * + */ +public class Word { + + private final List characters = new ArrayList<>(); + private double width = 0; + private final double fontSize; + + /** + * Create a new empty word. + * @param fontSize - the font size of the text which this word is in. + */ + protected Word(double fontSize){ + this.fontSize = fontSize; + } + + /** + * Adds a character to the end of the current word and increases the screen-space width of the word. + * @param character - the character to be added. + */ + protected void addCharacter(Character character){ + characters.add(character); + width += character.getxAdvance() * fontSize; + } + + /** + * @return The list of characters in the word. + */ + protected List getCharacters(){ + return characters; + } + + /** + * @return The width of the word in terms of screen size. + */ + protected double getWordWidth(){ + return width; + } + +} diff --git a/client/src/main/resources/shader/text/fragment.glsl b/client/src/main/resources/shader/text/fragment.glsl new file mode 100644 index 0000000..e69de29 diff --git a/client/src/main/resources/shader/text/vertex.glsl b/client/src/main/resources/shader/text/vertex.glsl new file mode 100644 index 0000000..e69de29 diff --git a/client/src/main/resources/text/jetbrains-mono.fnt b/client/src/main/resources/text/jetbrains-mono.fnt new file mode 100644 index 0000000..0a7414b --- /dev/null +++ b/client/src/main/resources/text/jetbrains-mono.fnt @@ -0,0 +1,102 @@ +info face="JetBrains Mono Regular" size=74 bold=0 italic=0 charset="" unicode=0 stretchH=100 smooth=1 aa=1 padding=3,3,3,3 spacing=0,0 +common lineHeight=105 base=76 scaleW=512 scaleH=512 pages=1 packed=0 +page id=0 file="jetbrains-mono.png" +chars count=97 +char id=0 x=0 y=444 width=48 height=26 xoffset=-1 yoffset=36 xadvance=50 page=0 chnl=0 +char id=10 x=0 y=0 width=0 height=0 xoffset=-3 yoffset=0 xadvance=6 page=0 chnl=0 +char id=32 x=0 y=0 width=0 height=0 xoffset=-3 yoffset=0 xadvance=50 page=0 chnl=0 +char id=33 x=490 y=82 width=18 height=62 xoffset=13 yoffset=18 xadvance=50 page=0 chnl=0 +char id=34 x=457 y=396 width=29 height=29 xoffset=8 yoffset=18 xadvance=50 page=0 chnl=0 +char id=36 x=0 y=0 width=41 height=82 xoffset=2 yoffset=7 xadvance=50 page=0 chnl=0 +char id=37 x=312 y=151 width=49 height=62 xoffset=-2 yoffset=18 xadvance=50 page=0 chnl=0 +char id=38 x=368 y=82 width=44 height=63 xoffset=1 yoffset=17 xadvance=50 page=0 chnl=0 +char id=39 x=486 y=396 width=14 height=29 xoffset=15 yoffset=18 xadvance=50 page=0 chnl=0 +char id=40 x=77 y=0 width=29 height=78 xoffset=10 yoffset=10 xadvance=50 page=0 chnl=0 +char id=41 x=106 y=0 width=29 height=78 xoffset=5 yoffset=10 xadvance=50 page=0 chnl=0 +char id=42 x=290 y=396 width=46 height=45 xoffset=-1 yoffset=27 xadvance=50 page=0 chnl=0 +char id=43 x=336 y=396 width=42 height=41 xoffset=1 yoffset=31 xadvance=50 page=0 chnl=0 +char id=44 x=483 y=0 width=22 height=30 xoffset=9 yoffset=61 xadvance=50 page=0 chnl=0 +char id=45 x=179 y=444 width=31 height=12 xoffset=7 yoffset=45 xadvance=50 page=0 chnl=0 +char id=46 x=137 y=444 width=19 height=19 xoffset=13 yoffset=61 xadvance=50 page=0 chnl=0 +char id=47 x=149 y=0 width=40 height=77 xoffset=2 yoffset=10 xadvance=50 page=0 chnl=0 +char id=48 x=328 y=82 width=40 height=63 xoffset=2 yoffset=17 xadvance=50 page=0 chnl=0 +char id=49 x=460 y=274 width=40 height=61 xoffset=3 yoffset=18 xadvance=50 page=0 chnl=0 +char id=50 x=77 y=151 width=40 height=62 xoffset=2 yoffset=17 xadvance=50 page=0 chnl=0 +char id=51 x=117 y=151 width=39 height=62 xoffset=2 yoffset=18 xadvance=50 page=0 chnl=0 +char id=52 x=0 y=335 width=38 height=61 xoffset=2 yoffset=18 xadvance=50 page=0 chnl=0 +char id=53 x=156 y=151 width=39 height=62 xoffset=3 yoffset=18 xadvance=50 page=0 chnl=0 +char id=54 x=195 y=151 width=42 height=62 xoffset=1 yoffset=18 xadvance=50 page=0 chnl=0 +char id=55 x=38 y=335 width=42 height=61 xoffset=2 yoffset=18 xadvance=50 page=0 chnl=0 +char id=56 x=286 y=82 width=42 height=63 xoffset=1 yoffset=17 xadvance=50 page=0 chnl=0 +char id=57 x=237 y=151 width=42 height=62 xoffset=1 yoffset=17 xadvance=50 page=0 chnl=0 +char id=58 x=483 y=151 width=19 height=49 xoffset=13 yoffset=31 xadvance=50 page=0 chnl=0 +char id=59 x=477 y=213 width=23 height=60 xoffset=9 yoffset=31 xadvance=50 page=0 chnl=0 +char id=60 x=0 y=0 width=0 height=0 xoffset=3 yoffset=0 xadvance=50 page=0 chnl=0 +char id=61 x=418 y=396 width=39 height=30 xoffset=3 yoffset=36 xadvance=50 page=0 chnl=0 +char id=62 x=165 y=335 width=110 height=50 xoffset=3 yoffset=27 xadvance=50 page=0 chnl=0 +char id=63 x=279 y=151 width=33 height=62 xoffset=6 yoffset=18 xadvance=50 page=0 chnl=0 +char id=64 x=396 y=0 width=45 height=75 xoffset=0 yoffset=17 xadvance=50 page=0 chnl=0 +char id=65 x=361 y=151 width=44 height=61 xoffset=0 yoffset=18 xadvance=50 page=0 chnl=0 +char id=66 x=405 y=151 width=40 height=61 xoffset=3 yoffset=18 xadvance=50 page=0 chnl=0 +char id=67 x=129 y=82 width=39 height=63 xoffset=3 yoffset=17 xadvance=50 page=0 chnl=0 +char id=68 x=445 y=151 width=38 height=61 xoffset=3 yoffset=18 xadvance=50 page=0 chnl=0 +char id=69 x=0 y=213 width=38 height=61 xoffset=4 yoffset=18 xadvance=50 page=0 chnl=0 +char id=70 x=38 y=213 width=38 height=61 xoffset=4 yoffset=18 xadvance=50 page=0 chnl=0 +char id=71 x=168 y=82 width=39 height=63 xoffset=3 yoffset=17 xadvance=50 page=0 chnl=0 +char id=72 x=76 y=213 width=38 height=61 xoffset=3 yoffset=18 xadvance=50 page=0 chnl=0 +char id=73 x=114 y=213 width=36 height=61 xoffset=4 yoffset=18 xadvance=50 page=0 chnl=0 +char id=74 x=412 y=82 width=40 height=62 xoffset=0 yoffset=18 xadvance=50 page=0 chnl=0 +char id=75 x=150 y=213 width=42 height=61 xoffset=3 yoffset=18 xadvance=50 page=0 chnl=0 +char id=76 x=192 y=213 width=38 height=61 xoffset=6 yoffset=18 xadvance=50 page=0 chnl=0 +char id=77 x=230 y=213 width=40 height=61 xoffset=2 yoffset=18 xadvance=50 page=0 chnl=0 +char id=78 x=270 y=213 width=38 height=61 xoffset=3 yoffset=18 xadvance=50 page=0 chnl=0 +char id=79 x=207 y=82 width=38 height=63 xoffset=3 yoffset=17 xadvance=50 page=0 chnl=0 +char id=80 x=308 y=213 width=41 height=61 xoffset=3 yoffset=18 xadvance=50 page=0 chnl=0 +char id=81 x=356 y=0 width=40 height=75 xoffset=2 yoffset=17 xadvance=50 page=0 chnl=0 +char id=82 x=349 y=213 width=41 height=61 xoffset=3 yoffset=18 xadvance=50 page=0 chnl=0 +char id=83 x=245 y=82 width=41 height=63 xoffset=2 yoffset=17 xadvance=50 page=0 chnl=0 +char id=84 x=390 y=213 width=43 height=61 xoffset=1 yoffset=18 xadvance=50 page=0 chnl=0 +char id=85 x=452 y=82 width=38 height=62 xoffset=3 yoffset=18 xadvance=50 page=0 chnl=0 +char id=86 x=433 y=213 width=44 height=61 xoffset=0 yoffset=18 xadvance=50 page=0 chnl=0 +char id=87 x=0 y=274 width=48 height=61 xoffset=-2 yoffset=18 xadvance=50 page=0 chnl=0 +char id=88 x=48 y=274 width=46 height=61 xoffset=-1 yoffset=18 xadvance=50 page=0 chnl=0 +char id=89 x=94 y=274 width=46 height=61 xoffset=-1 yoffset=18 xadvance=50 page=0 chnl=0 +char id=90 x=140 y=274 width=39 height=61 xoffset=3 yoffset=18 xadvance=50 page=0 chnl=0 +char id=91 x=229 y=0 width=25 height=76 xoffset=12 yoffset=11 xadvance=50 page=0 chnl=0 +char id=92 x=189 y=0 width=40 height=77 xoffset=2 yoffset=10 xadvance=50 page=0 chnl=0 +char id=93 x=254 y=0 width=25 height=76 xoffset=8 yoffset=11 xadvance=50 page=0 chnl=0 +char id=94 x=378 y=396 width=40 height=36 xoffset=2 yoffset=18 xadvance=50 page=0 chnl=0 +char id=95 x=0 y=82 width=129 height=69 xoffset=1 yoffset=18 xadvance=50 page=0 chnl=0 +char id=96 x=156 y=444 width=23 height=17 xoffset=8 yoffset=14 xadvance=50 page=0 chnl=0 +char id=97 x=275 y=335 width=40 height=49 xoffset=1 yoffset=31 xadvance=50 page=0 chnl=0 +char id=98 x=0 y=151 width=39 height=62 xoffset=3 yoffset=18 xadvance=50 page=0 chnl=0 +char id=99 x=315 y=335 width=39 height=49 xoffset=3 yoffset=31 xadvance=50 page=0 chnl=0 +char id=100 x=39 y=151 width=38 height=62 xoffset=3 yoffset=18 xadvance=50 page=0 chnl=0 +char id=101 x=354 y=335 width=39 height=49 xoffset=3 yoffset=31 xadvance=50 page=0 chnl=0 +char id=102 x=179 y=274 width=42 height=61 xoffset=1 yoffset=18 xadvance=50 page=0 chnl=0 +char id=103 x=221 y=274 width=38 height=61 xoffset=3 yoffset=31 xadvance=50 page=0 chnl=0 +char id=104 x=259 y=274 width=38 height=61 xoffset=3 yoffset=18 xadvance=50 page=0 chnl=0 +char id=105 x=441 y=0 width=42 height=65 xoffset=3 yoffset=14 xadvance=50 page=0 chnl=0 +char id=106 x=41 y=0 width=36 height=78 xoffset=2 yoffset=14 xadvance=50 page=0 chnl=0 +char id=107 x=297 y=274 width=41 height=61 xoffset=4 yoffset=18 xadvance=50 page=0 chnl=0 +char id=108 x=338 y=274 width=45 height=61 xoffset=-1 yoffset=18 xadvance=50 page=0 chnl=0 +char id=109 x=0 y=396 width=42 height=48 xoffset=1 yoffset=31 xadvance=50 page=0 chnl=0 +char id=110 x=471 y=335 width=38 height=48 xoffset=3 yoffset=31 xadvance=50 page=0 chnl=0 +char id=111 x=393 y=335 width=39 height=49 xoffset=3 yoffset=31 xadvance=50 page=0 chnl=0 +char id=112 x=383 y=274 width=39 height=61 xoffset=3 yoffset=31 xadvance=50 page=0 chnl=0 +char id=113 x=422 y=274 width=38 height=61 xoffset=3 yoffset=31 xadvance=50 page=0 chnl=0 +char id=114 x=42 y=396 width=38 height=48 xoffset=5 yoffset=31 xadvance=50 page=0 chnl=0 +char id=115 x=432 y=335 width=39 height=49 xoffset=3 yoffset=31 xadvance=50 page=0 chnl=0 +char id=116 x=124 y=335 width=41 height=59 xoffset=1 yoffset=20 xadvance=50 page=0 chnl=0 +char id=117 x=80 y=396 width=38 height=48 xoffset=3 yoffset=32 xadvance=50 page=0 chnl=0 +char id=118 x=118 y=396 width=44 height=47 xoffset=0 yoffset=32 xadvance=50 page=0 chnl=0 +char id=119 x=162 y=396 width=46 height=47 xoffset=-1 yoffset=32 xadvance=50 page=0 chnl=0 +char id=120 x=208 y=396 width=44 height=47 xoffset=0 yoffset=32 xadvance=50 page=0 chnl=0 +char id=121 x=80 y=335 width=44 height=60 xoffset=0 yoffset=32 xadvance=50 page=0 chnl=0 +char id=122 x=252 y=396 width=38 height=47 xoffset=3 yoffset=32 xadvance=50 page=0 chnl=0 +char id=123 x=279 y=0 width=39 height=76 xoffset=2 yoffset=11 xadvance=50 page=0 chnl=0 +char id=124 x=135 y=0 width=14 height=77 xoffset=15 yoffset=10 xadvance=50 page=0 chnl=0 +char id=125 x=318 y=0 width=38 height=76 xoffset=4 yoffset=11 xadvance=50 page=0 chnl=0 +char id=126 x=96 y=444 width=41 height=22 xoffset=2 yoffset=38 xadvance=50 page=0 chnl=0 +char id=127 x=48 y=444 width=48 height=25 xoffset=-1 yoffset=36 xadvance=50 page=0 chnl=0 +kernings count=0 diff --git a/client/src/main/resources/text/jetbrains-mono.png b/client/src/main/resources/text/jetbrains-mono.png new file mode 100644 index 0000000..816b567 Binary files /dev/null and b/client/src/main/resources/text/jetbrains-mono.png differ diff --git a/core/src/main/java/nl/andrewl/aos_core/model/Chunk.java b/core/src/main/java/nl/andrewl/aos_core/model/Chunk.java index 1478b7d..ef16e08 100644 --- a/core/src/main/java/nl/andrewl/aos_core/model/Chunk.java +++ b/core/src/main/java/nl/andrewl/aos_core/model/Chunk.java @@ -126,14 +126,4 @@ public class Chunk { } return c; } - - public static Vector3f getColor(byte blockValue) { - float v = blockValue / 127.0f; - return new Vector3f(v); - } - - public static void getColor(byte blockValue, Vector3f vec) { - float v = blockValue / 127.0f; - vec.set(v); - } } 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 0e002a6..a3d5ca4 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 @@ -56,9 +56,7 @@ public class World { getChunkPosAt(pos, util); Chunk chunk = chunkMap.get(util); if (chunk == null) return 0; - 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); + getLocalPosAt(pos.x, pos.y, pos.z, util); return chunk.getBlockAt(util); } @@ -70,11 +68,7 @@ public class World { Vector3i chunkPos = getChunkPosAt(pos); Chunk chunk = chunkMap.get(chunkPos); if (chunk == null) return; - 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) - ); + Vector3i blockPos = getLocalPosAt(pos.x, pos.y, pos.z, chunkPos); chunk.setBlockAt(blockPos.x, blockPos.y, blockPos.z, block); } @@ -86,6 +80,10 @@ public class World { return chunkMap.get(chunkPos); } + public Chunk getChunkAt(int x, int y, int z) { + return chunkMap.get(new Vector3i(x, y, z)); + } + /** * Gets the position that a system is looking at, within a distance limit. * Usually used to determine where a player has interacted/clicked in the @@ -157,18 +155,52 @@ public class World { } /** - * Gets the coordinates of a chunk at a given world position. - * @param worldPos The world position. - * @return The chunk position. Note that this may not correspond to any existing chunk. + * Gets the chunk position at the specified world position. + * @param x The x coordinate. + * @param y The y coordinate. + * @param z The z coordinate. + * @param dest The destination vector to place the chunk position in. + * @return The destination vector, for method chaining. */ + public static Vector3i getChunkPosAt(float x, float y, float z, Vector3i dest) { + dest.x = (int) Math.floor(x / Chunk.SIZE); + dest.y = (int) Math.floor(y / Chunk.SIZE); + dest.z = (int) Math.floor(z / Chunk.SIZE); + return dest; + } + + public static Vector3i getChunkPosAt(Vector3f worldPos, Vector3i dest) { + return getChunkPosAt(worldPos.x, worldPos.y, worldPos.z, dest); + } + public static Vector3i getChunkPosAt(Vector3f worldPos) { 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); + public static Vector3i getChunkPosAt(Vector3i worldPos) { + return getChunkPosAt(worldPos.x, worldPos.y, worldPos.z, new Vector3i()); + } + + /** + * Gets the chunk-local position at the specified world position. + * @param x The x coordinate. + * @param y The y coordinate. + * @param z The z coordinate. + * @param dest The destination vector to place the local position in. + * @return The destination vector, for method chaining. + */ + public static Vector3i getLocalPosAt(float x, float y, float z, Vector3i dest) { + getChunkPosAt(x, y, z, dest); + float chunkX = dest.x; + float chunkY = dest.y; + float chunkZ = dest.z; + dest.x = (int) Math.floor(x - chunkX * Chunk.SIZE); + dest.y = (int) Math.floor(y - chunkY * Chunk.SIZE); + dest.z = (int) Math.floor(z - chunkZ * Chunk.SIZE); return dest; } + + public static Vector3i getLocalPosAt(Vector3i worldPos) { + return getLocalPosAt(worldPos.x, worldPos.y, worldPos.z, new Vector3i()); + } } diff --git a/core/src/main/java/nl/andrewl/aos_core/net/udp/ClientInputState.java b/core/src/main/java/nl/andrewl/aos_core/net/udp/ClientInputState.java index 3775eb0..c409d77 100644 --- a/core/src/main/java/nl/andrewl/aos_core/net/udp/ClientInputState.java +++ b/core/src/main/java/nl/andrewl/aos_core/net/udp/ClientInputState.java @@ -3,16 +3,21 @@ package nl.andrewl.aos_core.net.udp; import nl.andrewl.record_net.Message; /** - * A message that' sent periodically by the client when the player's input + * A message that's sent periodically by the client when the player's input * changes. */ public record ClientInputState( int clientId, + // Movement boolean forward, boolean backward, boolean left, boolean right, boolean jumping, boolean crouching, - boolean sprinting + boolean sprinting, + + // Interaction + boolean hitting, // Usually a "left-click" action. + boolean interacting // Usually a "right-click" action. ) implements Message {} 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 8fa9493..60c0551 100644 --- a/server/src/main/java/nl/andrewl/aos2_server/ServerPlayer.java +++ b/server/src/main/java/nl/andrewl/aos2_server/ServerPlayer.java @@ -1,11 +1,14 @@ package nl.andrewl.aos2_server; +import nl.andrewl.aos_core.model.Chunk; import nl.andrewl.aos_core.model.Player; import nl.andrewl.aos_core.model.World; +import nl.andrewl.aos_core.net.udp.ChunkUpdateMessage; import nl.andrewl.aos_core.net.udp.ClientInputState; import org.joml.Math; import org.joml.Vector2i; import org.joml.Vector3f; +import org.joml.Vector3i; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,6 +20,8 @@ public class ServerPlayer extends Player { public static final float HEIGHT = 1.8f; public static final float HEIGHT_CROUCH = 1.4f; + public static final float EYE_HEIGHT = HEIGHT - 0.1f; + public static final float EYE_HEIGHT_CROUCH = HEIGHT_CROUCH - 0.1f; public static final float WIDTH = 0.75f; public static final float RADIUS = WIDTH / 2f; @@ -28,13 +33,17 @@ public class ServerPlayer extends Player { public static final float MOVEMENT_DECELERATION = 2f; public static final float JUMP_SPEED = 8f; + public static final int BLOCK_REMOVE_COOLDOWN = 250; + private ClientInputState lastInputState; + private long lastBlockRemovedAt = 0; + private boolean updated = false; public ServerPlayer(int id, String username) { super(id, username); // Initialize with a default state of no input. - lastInputState = new ClientInputState(id, false, false, false, false, false, false, false); + lastInputState = new ClientInputState(id, false, false, false, false, false, false, false, false, false); } public ClientInputState getLastInputState() { @@ -49,13 +58,36 @@ public class ServerPlayer extends Player { return updated; } - public void tick(float dt, World world) { + public void tick(float dt, World world, Server server) { + long now = System.currentTimeMillis(); + if (lastInputState.hitting() && now - lastBlockRemovedAt > BLOCK_REMOVE_COOLDOWN) { + Vector3f eyePos = new Vector3f(position); + eyePos.y += getEyeHeight(); + Vector3i targetPos = world.getLookingAtPos(eyePos, viewVector, 10); + System.out.println(targetPos); + if (targetPos != null) { + Vector3i chunkPos = World.getChunkPosAt(targetPos); + Vector3i localPos = World.getLocalPosAt(targetPos); + world.setBlockAt(targetPos.x, targetPos.y, targetPos.z, (byte) 0); + lastBlockRemovedAt = now; + server.getPlayerManager().broadcastUdpMessage(new ChunkUpdateMessage( + chunkPos.x, chunkPos.y, chunkPos.z, + localPos.x, localPos.y, localPos.z, + (byte) 0 + )); + } + } + tickMovement(dt, world); + } + + private void tickMovement(float dt, World world) { updated = false; // Reset the updated flag. This will be set to true if the player was updated in this tick. + boolean grounded = isGrounded(world); + tickHorizontalVelocity(grounded); if (isGrounded(world)) { - tickHorizontalVelocity(); if (lastInputState.jumping()) { - velocity.y = JUMP_SPEED; + velocity.y = JUMP_SPEED * (lastInputState.sprinting() ? 1.25f : 1f); updated = true; } } else { @@ -73,7 +105,7 @@ public class ServerPlayer extends Player { } } - private void tickHorizontalVelocity() { + private void tickHorizontalVelocity(boolean doDeceleration) { Vector3f horizontalVelocity = new Vector3f( velocity.x == velocity.x ? velocity.x : 0f, 0, @@ -101,7 +133,7 @@ public class ServerPlayer extends Player { horizontalVelocity.normalize(maxSpeed); } updated = true; - } else if (horizontalVelocity.lengthSquared() > 0) { + } else if (doDeceleration && horizontalVelocity.lengthSquared() > 0) { Vector3f deceleration = new Vector3f(horizontalVelocity) .negate().normalize() .mul(Math.min(horizontalVelocity.length(), MOVEMENT_DECELERATION)); @@ -154,105 +186,144 @@ public class ServerPlayer extends Player { // movement.x, movement.y, movement.z, // nextTickPosition.x, nextTickPosition.y, nextTickPosition.z // ); - checkWallCollision(world, nextTickPosition, movement); - checkCeilingCollision(world, nextTickPosition, movement); - checkFloorCollision(world, nextTickPosition, movement); - } + float height = getCurrentHeight(); + float delta = 0.00001f; + final Vector3f stepSize = new Vector3f(movement).normalize(1.0f); + // The number of steps we'll make towards the next tick position. + int stepCount = (int) Math.ceil(movement.length()); + if (stepCount == 0) return; // No movement, so exit. + final Vector3f nextPos = new Vector3f(position); + final Vector3f lastPos = new Vector3f(position); + for (int i = 0; i < stepCount; i++) { + lastPos.set(nextPos); + nextPos.add(stepSize); + // If we shot past the next tick position, clamp it to that. + if (new Vector3f(nextPos).sub(position).length() > movement.length()) { + nextPos.set(nextTickPosition); + } - private void checkFloorCollision(World world, Vector3f nextTickPosition, Vector3f movement) { - // If the player is moving up or not falling out of their current y level, no point in checking. - if (velocity.y >= 0 || Math.floor(position.y) == Math.floor(nextTickPosition.y)) return; - float dropHeight = Math.abs(movement.y); - int steps = (int) Math.ceil(dropHeight); -// System.out.printf(" dropHeight=%.3f, steps=%d%n", dropHeight, steps); - // Get a vector describing how much we move for each 1 unit Y decreases. - Vector3f stepSize = new Vector3f(movement).div(dropHeight); - Vector3f potentialPosition = new Vector3f(position); - for (int i = 0; i < steps; i++) { - potentialPosition.add(stepSize); -// System.out.printf(" Checking: %.3f, %.3f, %.3f%n", potentialPosition.x, potentialPosition.y, potentialPosition.z); - if (getHorizontalSpaceOccupied(potentialPosition).stream() - .anyMatch(p -> world.getBlockAt(p.x, potentialPosition.y, p.y) != 0)) { -// System.out.println(" Occupied!"); - position.y = Math.ceil(potentialPosition.y); - velocity.y = 0; - movement.y = 0; - updated = true; - return; // Exit before doing any extra work. + // Check if we collide with anything at this new position. + + + float playerBodyPrevMinZ = lastPos.z - RADIUS; + float playerBodyPrevMaxZ = lastPos.z + RADIUS; + float playerBodyPrevMinX = lastPos.x - RADIUS; + float playerBodyPrevMaxX = lastPos.x + RADIUS; + float playerBodyPrevMinY = lastPos.y; + float playerBodyPrevMaxY = lastPos.y + height; + + float playerBodyMinZ = nextPos.z - RADIUS; + float playerBodyMaxZ = nextPos.z + RADIUS; + float playerBodyMinX = nextPos.x - RADIUS; + float playerBodyMaxX = nextPos.x + RADIUS; + float playerBodyMinY = nextPos.y; + float playerBodyMaxY = nextPos.y + height; + + // Compute the bounds of all blocks the player is intersecting with. + int minX = (int) Math.floor(playerBodyMinX); + int minZ = (int) Math.floor(playerBodyMinZ); + int minY = (int) Math.floor(playerBodyMinY); + int maxX = (int) Math.floor(playerBodyMaxX); + int maxZ = (int) Math.floor(playerBodyMaxZ); + int maxY = (int) Math.floor(playerBodyMaxY); + + for (int x = minX; x <= maxX; x++) { + for (int z = minZ; z <= maxZ; z++) { + for (int y = minY; y <= maxY; y++) { + byte block = world.getBlockAt(x, y, z); + if (block <= 0) continue; // We're not colliding with this block. + float blockMinY = (float) y; + float blockMaxY = (float) y + 1; + float blockMinX = (float) x; + float blockMaxX = (float) x + 1; + float blockMinZ = (float) z; + float blockMaxZ = (float) z + 1; + + /* + To determine if the player is moving into the -Z side of a block: + - The player's max z position went from < blockMinZ to >= blockMinZ. + - The block to the -Z direction is air. + */ + boolean collidingWithNegativeZ = playerBodyPrevMaxZ < blockMinZ && playerBodyMaxZ >= blockMinZ && world.getBlockAt(x, y, z - 1) <= 0; + if (collidingWithNegativeZ) { + position.z = blockMinZ - RADIUS - delta; + velocity.z = 0; + movement.z = 0; + } + + /* + To determine if the player is moving into the +Z side of a block: + - The player's min z position went from >= blockMaxZ to < blockMaxZ. + - The block to the +Z direction is air. + */ + boolean collidingWithPositiveZ = playerBodyPrevMinZ >= blockMaxZ && playerBodyMinZ < blockMaxZ && world.getBlockAt(x, y, z + 1) <= 0; + if (collidingWithPositiveZ) { + position.z = blockMaxZ + RADIUS + delta; + velocity.z = 0; + movement.z = 0; + } + + /* + To determine if the player is moving into the -X side of a block: + - The player's max x position went from < blockMinX to >= blockMinX + - The block to the -X direction is air. + */ + boolean collidingWithNegativeX = playerBodyPrevMaxX < blockMinX && playerBodyMaxX >= blockMinX && world.getBlockAt(x - 1, y, z) <= 0; + if (collidingWithNegativeX) { + position.x = blockMinX - RADIUS - delta; + velocity.x = 0; + movement.x = 0; + } + + /* + To determine if the player is moving into the +X side of a block: + - The player's min x position went from >= blockMaxX to < blockMaxX. + - The block to the +X direction is air. + */ + boolean collidingWithPositiveX = playerBodyPrevMinX >= blockMaxX && playerBodyMinX < blockMaxX && world.getBlockAt(x + 1, y, z) <= 0; + if (collidingWithPositiveX) { + position.x = blockMaxX + RADIUS + delta; + velocity.x = 0; + movement.x = 0; + } + + /* + To determine if the player is moving down onto a block: + - The player's min y position went from >= blockMaxY to < blockMaxY + - The block above the current one is air. + */ + boolean collidingWithFloor = playerBodyPrevMinY >= blockMaxY && playerBodyMinY < blockMaxY && world.getBlockAt(x, y + 1, z) <= 0; + if (collidingWithFloor) { + position.y = blockMaxY; + velocity.y = 0; + movement.y = 0; + } + + /* + To determine if the player is moving up into a block: + - The player's y position went from below blockMinY to >= blockMinY + - The block below the current one is air. + */ + boolean collidingWithCeiling = playerBodyPrevMaxY < blockMinY && playerBodyMaxY >= blockMinY && world.getBlockAt(x, y - 1, z) <= 0; + if (collidingWithCeiling) { + position.y = blockMinY - height - delta; + velocity.y = 0; + movement.y = 0; + } + + updated = true; + } + } } } } - private void checkCeilingCollision(World world, Vector3f nextTickPosition, Vector3f movement) { - // If the player is moving down, or not moving out of their current y level, no point in checking. - if (velocity.y <= 0 || Math.floor(position.y) == Math.floor(nextTickPosition.y)) return; - float riseHeight = Math.abs(movement.y); - int steps = (int) Math.ceil(riseHeight); - Vector3f stepSize = new Vector3f(movement).div(riseHeight); - Vector3f potentialPosition = new Vector3f(position); - float playerHeight = lastInputState.crouching() ? HEIGHT_CROUCH : HEIGHT; - for (int i = 0; i < steps; i++) { - potentialPosition.add(stepSize); - if (getHorizontalSpaceOccupied(potentialPosition).stream() - .anyMatch(p -> world.getBlockAt(p.x, potentialPosition.y + playerHeight, p.y) != 0)) { - position.y = Math.floor(potentialPosition.y); - velocity.y = 0; - movement.y = 0; - updated = true; - return; // Exit before doing any extra work. - } - } + public float getCurrentHeight() { + return lastInputState.crouching() ? HEIGHT_CROUCH : HEIGHT; } - private void checkWallCollision(World world, Vector3f nextTickPosition, Vector3f movement) { - // If the player isn't moving horizontally, no point in checking. - if (velocity.x == 0 && velocity.z == 0) return; - Vector3f potentialPosition = new Vector3f(position); - Vector3f stepSize = new Vector3f(movement).normalize(); // Step by 1 meter each time. This will guarantee we check everything, no matter what. - int steps = (int) Math.ceil(movement.length()); - for (int i = 0; i < steps; i++) { - potentialPosition.add(stepSize); - float x = potentialPosition.x; - float y = potentialPosition.y + 1f; - float z = potentialPosition.z; - - float borderMinZ = z - RADIUS; - float borderMaxZ = z + RADIUS; - float borderMinX = x - RADIUS; - float borderMaxX = x + RADIUS; - - // -Z - if (world.getBlockAt(x, y, borderMinZ) != 0) { - System.out.println("-z"); - position.z = Math.ceil(borderMinZ) + RADIUS; - velocity.z = 0; - movement.z = 0; - updated = true; - } - // +Z - if (world.getBlockAt(x, y, borderMaxZ) != 0) { - System.out.println("+z"); - position.z = Math.floor(borderMaxZ) - RADIUS; - velocity.z = 0; - movement.z = 0; - updated = true; - } - // -X - if (world.getBlockAt(borderMinX, y, z) != 0) { - System.out.println("-x"); - position.x = Math.ceil(borderMinX) + RADIUS; - velocity.x = 0; - movement.z = 0; - updated = true; - } - // +X - if (world.getBlockAt(borderMaxX, y, z) != 0) { - System.out.println("+x"); - position.x = Math.floor(borderMaxX) - RADIUS; - velocity.x = 0; - movement.x = 0; - updated = true; - } - } + public float getEyeHeight() { + return lastInputState.crouching() ? EYE_HEIGHT_CROUCH : EYE_HEIGHT; } + } 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 1546f7f..e3086e7 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,7 @@ public class WorldUpdater implements Runnable { private void tick() { for (var player : server.getPlayerManager().getPlayers()) { - player.tick(secondsPerTick, server.getWorld()); + player.tick(secondsPerTick, server.getWorld(), server); if (player.isUpdated()) server.getPlayerManager().broadcastUdpMessage(new PlayerUpdateMessage( player.getId(), player.getPosition().x, player.getPosition().y, player.getPosition().z,