From 2ebf5ad1bfa4c5e391414ccabc16297996d62b7b Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Fri, 15 Jul 2022 19:46:49 +0200 Subject: [PATCH] Added block breaking stuff. --- .../java/nl/andrewl/aos2_client/Client.java | 25 +- .../aos2_client/control/InputHandler.java | 39 +++ .../control/PlayerInputKeyCallback.java | 25 +- .../PlayerInputMouseClickCallback.java | 20 ++ .../control/PlayerViewCursorCallback.java | 4 + .../andrewl/aos2_client/render/ChunkMesh.java | 11 +- .../aos2_client/render/ChunkRenderer.java | 4 +- .../aos2_client/render/GameRenderer.java | 17 +- .../aos2_client/render/font/Character.java | 104 +++++++ .../aos2_client/render/font/FontRenderer.java | 20 ++ .../aos2_client/render/font/FontType.java | 52 ++++ .../aos2_client/render/font/GUIText.java | 196 +++++++++++++ .../andrewl/aos2_client/render/font/Line.java | 77 +++++ .../aos2_client/render/font/MetaFile.java | 213 ++++++++++++++ .../render/font/TextMeshCreator.java | 133 +++++++++ .../aos2_client/render/font/TextMeshData.java | 30 ++ .../andrewl/aos2_client/render/font/Word.java | 48 ++++ .../main/resources/shader/text/fragment.glsl | 0 .../main/resources/shader/text/vertex.glsl | 0 .../main/resources/text/jetbrains-mono.fnt | 102 +++++++ .../main/resources/text/jetbrains-mono.png | Bin 0 -> 35287 bytes .../java/nl/andrewl/aos_core/model/Chunk.java | 10 - .../java/nl/andrewl/aos_core/model/World.java | 62 +++- .../aos_core/net/udp/ClientInputState.java | 9 +- .../nl/andrewl/aos2_server/ServerPlayer.java | 269 +++++++++++------- .../nl/andrewl/aos2_server/WorldUpdater.java | 2 +- 26 files changed, 1307 insertions(+), 165 deletions(-) create mode 100644 client/src/main/java/nl/andrewl/aos2_client/control/InputHandler.java create mode 100644 client/src/main/java/nl/andrewl/aos2_client/control/PlayerInputMouseClickCallback.java create mode 100644 client/src/main/java/nl/andrewl/aos2_client/render/font/Character.java create mode 100644 client/src/main/java/nl/andrewl/aos2_client/render/font/FontRenderer.java create mode 100644 client/src/main/java/nl/andrewl/aos2_client/render/font/FontType.java create mode 100644 client/src/main/java/nl/andrewl/aos2_client/render/font/GUIText.java create mode 100644 client/src/main/java/nl/andrewl/aos2_client/render/font/Line.java create mode 100644 client/src/main/java/nl/andrewl/aos2_client/render/font/MetaFile.java create mode 100644 client/src/main/java/nl/andrewl/aos2_client/render/font/TextMeshCreator.java create mode 100644 client/src/main/java/nl/andrewl/aos2_client/render/font/TextMeshData.java create mode 100644 client/src/main/java/nl/andrewl/aos2_client/render/font/Word.java create mode 100644 client/src/main/resources/shader/text/fragment.glsl create mode 100644 client/src/main/resources/shader/text/vertex.glsl create mode 100644 client/src/main/resources/text/jetbrains-mono.fnt create mode 100644 client/src/main/resources/text/jetbrains-mono.png 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 0000000000000000000000000000000000000000..816b56714bc94cc0d6a30d60f5c9b21e7322e6cc GIT binary patch literal 35287 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 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,