Added block breaking stuff.

This commit is contained in:
Andrew Lalis 2022-07-15 19:46:49 +02:00
parent c9b325da3a
commit 2ebf5ad1bf
26 changed files with 1307 additions and 165 deletions

View File

@ -1,15 +1,20 @@
package nl.andrewl.aos2_client; 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.PlayerInputKeyCallback;
import nl.andrewl.aos2_client.control.PlayerInputMouseClickCallback;
import nl.andrewl.aos2_client.control.PlayerViewCursorCallback; import nl.andrewl.aos2_client.control.PlayerViewCursorCallback;
import nl.andrewl.aos2_client.render.GameRenderer; import nl.andrewl.aos2_client.render.GameRenderer;
import nl.andrewl.aos_core.model.Chunk; import nl.andrewl.aos_core.model.Chunk;
import nl.andrewl.aos_core.model.ColorPalette; import nl.andrewl.aos_core.model.ColorPalette;
import nl.andrewl.aos_core.model.World; import nl.andrewl.aos_core.model.World;
import nl.andrewl.aos_core.net.ChunkDataMessage; import nl.andrewl.aos_core.net.ChunkDataMessage;
import nl.andrewl.aos_core.net.ChunkHashMessage;
import nl.andrewl.aos_core.net.WorldInfoMessage; 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.aos_core.net.udp.PlayerUpdateMessage;
import nl.andrewl.record_net.Message; import nl.andrewl.record_net.Message;
import org.joml.Vector3i;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -25,6 +30,7 @@ public class Client implements Runnable {
private final String username; private final String username;
private final CommunicationHandler communicationHandler; private final CommunicationHandler communicationHandler;
private final InputHandler inputHandler;
private final GameRenderer gameRenderer; private final GameRenderer gameRenderer;
private int clientId; private int clientId;
@ -35,6 +41,7 @@ public class Client implements Runnable {
this.serverPort = serverPort; this.serverPort = serverPort;
this.username = username; this.username = username;
this.communicationHandler = new CommunicationHandler(this); this.communicationHandler = new CommunicationHandler(this);
this.inputHandler = new InputHandler(communicationHandler);
this.world = new ClientWorld(); this.world = new ClientWorld();
this.gameRenderer = new GameRenderer(world); this.gameRenderer = new GameRenderer(world);
} }
@ -52,7 +59,8 @@ public class Client implements Runnable {
gameRenderer.setupWindow( gameRenderer.setupWindow(
new PlayerViewCursorCallback(gameRenderer.getCamera(), communicationHandler), new PlayerViewCursorCallback(gameRenderer.getCamera(), communicationHandler),
new PlayerInputKeyCallback(communicationHandler) new PlayerInputKeyCallback(inputHandler),
new PlayerInputMouseClickCallback(inputHandler)
); );
long lastFrameAt = System.currentTimeMillis(); long lastFrameAt = System.currentTimeMillis();
@ -82,11 +90,22 @@ public class Client implements Runnable {
if (msg instanceof ChunkDataMessage chunkDataMessage) { if (msg instanceof ChunkDataMessage chunkDataMessage) {
Chunk chunk = chunkDataMessage.toChunk(); Chunk chunk = chunkDataMessage.toChunk();
world.addChunk(chunk); 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 (msg instanceof PlayerUpdateMessage playerUpdate) {
if (playerUpdate.clientId() == clientId) { 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().setPosition(playerUpdate.px(), playerUpdate.py() + eyeHeight, playerUpdate.pz());
gameRenderer.getCamera().setVelocity(playerUpdate.vx(), playerUpdate.vy(), playerUpdate.vz()); gameRenderer.getCamera().setVelocity(playerUpdate.vx(), playerUpdate.vy(), playerUpdate.vz());
// TODO: Unload far away chunks and request close chunks we don't have. // TODO: Unload far away chunks and request close chunks we don't have.

View File

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

View File

@ -1,17 +1,14 @@
package nl.andrewl.aos2_client.control; 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 org.lwjgl.glfw.GLFWKeyCallbackI;
import static org.lwjgl.glfw.GLFW.*; import static org.lwjgl.glfw.GLFW.*;
public class PlayerInputKeyCallback implements GLFWKeyCallbackI { public class PlayerInputKeyCallback implements GLFWKeyCallbackI {
private ClientInputState lastInputState = null; private final InputHandler inputHandler;
private final CommunicationHandler comm;
public PlayerInputKeyCallback(CommunicationHandler comm) { public PlayerInputKeyCallback(InputHandler inputHandler) {
this.comm = comm; this.inputHandler = inputHandler;
} }
@Override @Override
@ -19,20 +16,6 @@ public class PlayerInputKeyCallback implements GLFWKeyCallbackI {
if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE) { if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE) {
glfwSetWindowShouldClose(window, true); glfwSetWindowShouldClose(window, true);
} }
inputHandler.updateInputState(window);
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;
}
} }
} }

View File

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

View File

@ -9,6 +9,10 @@ import java.util.concurrent.ForkJoinPool;
import static org.lwjgl.glfw.GLFW.glfwGetCursorPos; 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 { public class PlayerViewCursorCallback implements GLFWCursorPosCallbackI {
/** /**
* The number of milliseconds to wait before sending orientation updates, * The number of milliseconds to wait before sending orientation updates,

View File

@ -2,6 +2,8 @@ package nl.andrewl.aos2_client.render;
import nl.andrewl.aos_core.model.Chunk; import nl.andrewl.aos_core.model.Chunk;
import nl.andrewl.aos_core.model.World; import nl.andrewl.aos_core.model.World;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.lwjgl.opengl.GL46.*; import static org.lwjgl.opengl.GL46.*;
@ -9,6 +11,8 @@ import static org.lwjgl.opengl.GL46.*;
* Represents a 3d mesh for a chunk. * Represents a 3d mesh for a chunk.
*/ */
public class ChunkMesh { public class ChunkMesh {
private static final Logger log = LoggerFactory.getLogger(ChunkMesh.class);
private final int vboId; private final int vboId;
private final int vaoId; private final int vaoId;
private final int eboId; private final int eboId;
@ -50,12 +54,11 @@ public class ChunkMesh {
double dur = (System.nanoTime() - start) / 1_000_000.0; double dur = (System.nanoTime() - start) / 1_000_000.0;
this.indexCount = meshData.indexBuffer().limit(); this.indexCount = meshData.indexBuffer().limit();
// Print some debug information. // Print some debug information.
System.out.printf( log.debug(
"Generated mesh for chunk (%d, %d, %d) in %.3f ms. %d vertices, %d indices.%n", "Generated mesh for chunk ({}, {}, {}) in {} ms. {} vertices and {} indices.",
chunk.getPosition().x, chunk.getPosition().y, chunk.getPosition().z, chunk.getPosition().x, chunk.getPosition().y, chunk.getPosition().z,
dur, dur,
meshData.vertexBuffer().limit() / 9, meshData.vertexBuffer().limit() / 9, indexCount
indexCount
); );
glBindBuffer(GL_ARRAY_BUFFER, vboId); glBindBuffer(GL_ARRAY_BUFFER, vboId);

View File

@ -42,7 +42,7 @@ public class ChunkRenderer {
glUniform1i(chunkSizeUniform, Chunk.SIZE); glUniform1i(chunkSizeUniform, Chunk.SIZE);
} }
public void addChunkMesh(Chunk chunk) { public void queueChunkMesh(Chunk chunk) {
meshGenerationQueue.add(chunk); meshGenerationQueue.add(chunk);
} }
@ -54,6 +54,8 @@ public class ChunkRenderer {
while (!meshGenerationQueue.isEmpty()) { while (!meshGenerationQueue.isEmpty()) {
Chunk chunk = meshGenerationQueue.remove(); Chunk chunk = meshGenerationQueue.remove();
ChunkMesh mesh = new ChunkMesh(chunk, world, chunkMeshGenerator); ChunkMesh mesh = new ChunkMesh(chunk, world, chunkMeshGenerator);
ChunkMesh existingMesh = chunkMeshes.get(chunk.getPosition());
if (existingMesh != null) existingMesh.free();
chunkMeshes.put(chunk.getPosition(), mesh); chunkMeshes.put(chunk.getPosition(), mesh);
} }
shaderProgram.use(); shaderProgram.use();

View File

@ -1,14 +1,9 @@
package nl.andrewl.aos2_client.render; package nl.andrewl.aos2_client.render;
import nl.andrewl.aos2_client.Camera; 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 nl.andrewl.aos_core.model.World;
import org.joml.Matrix4f; import org.joml.Matrix4f;
import org.lwjgl.glfw.Callbacks; import org.lwjgl.glfw.*;
import org.lwjgl.glfw.GLFWErrorCallback;
import org.lwjgl.glfw.GLFWVidMode;
import org.lwjgl.opengl.GL; import org.lwjgl.opengl.GL;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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(); GLFWErrorCallback.createPrint(System.err).set();
if (!glfwInit()) throw new IllegalStateException("Could not initialize GLFW."); if (!glfwInit()) throw new IllegalStateException("Could not initialize GLFW.");
glfwDefaultWindowHints(); glfwDefaultWindowHints();
@ -65,6 +60,7 @@ public class GameRenderer {
// Setup callbacks. // Setup callbacks.
glfwSetKeyCallback(windowHandle, inputKeyCallback); glfwSetKeyCallback(windowHandle, inputKeyCallback);
glfwSetCursorPosCallback(windowHandle, viewCursorCallback); glfwSetCursorPosCallback(windowHandle, viewCursorCallback);
glfwSetMouseButtonCallback(windowHandle, mouseButtonCallback);
glfwSetInputMode(windowHandle, GLFW_CURSOR, GLFW_CURSOR_DISABLED); glfwSetInputMode(windowHandle, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
glfwSetInputMode(windowHandle, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE); glfwSetInputMode(windowHandle, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE);
glfwSetCursorPos(windowHandle, 0, 0); glfwSetCursorPos(windowHandle, 0, 0);
@ -118,13 +114,16 @@ public class GameRenderer {
updatePerspective(); updatePerspective();
} }
public float getAspectRatio() {
return (float) screenWidth / (float) screenHeight;
}
/** /**
* Updates the rendering perspective used to render the game. Note: only * Updates the rendering perspective used to render the game. Note: only
* call this after calling {@link ChunkRenderer#setupShaderProgram()}. * call this after calling {@link ChunkRenderer#setupShaderProgram()}.
*/ */
private void updatePerspective() { private void updatePerspective() {
float aspect = (float) screenWidth / (float) screenHeight; perspectiveTransform.setPerspective(fov, getAspectRatio(), Z_NEAR, Z_FAR);
perspectiveTransform.setPerspective(fov, aspect, Z_NEAR, Z_FAR);
chunkRenderer.setPerspective(perspectiveTransform); chunkRenderer.setPerspective(perspectiveTransform);
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Word> words = new ArrayList<Word>();
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<Word> getWords() {
return words;
}
}

View File

@ -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<Integer, Character> metaData = new HashMap<>();
private BufferedReader reader;
private final Map<String, String> 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);
}
}

View File

@ -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<Line> lines = createStructure(text);
return createQuadVertices(text, lines);
}
private List<Line> createStructure(GUIText text) {
char[] chars = text.getTextString().toCharArray();
List<Line> lines = new ArrayList<Line>();
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<Line> 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<Line> lines) {
text.setNumberOfLines(lines.size());
double curserX = 0f;
double curserY = 0f;
List<Float> vertices = new ArrayList<Float>();
List<Float> textureCoords = new ArrayList<Float>();
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<Float> 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<Float> 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<Float> 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<Float> listOfFloats) {
float[] array = new float[listOfFloats.size()];
for (int i = 0; i < array.length; i++) {
array[i] = listOfFloats.get(i);
}
return array;
}
}

View File

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

View File

@ -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<Character> 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<Character> getCharacters(){
return characters;
}
/**
* @return The width of the word in terms of screen size.
*/
protected double getWordWidth(){
return width;
}
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -126,14 +126,4 @@ public class Chunk {
} }
return c; 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);
}
} }

View File

@ -56,9 +56,7 @@ public class World {
getChunkPosAt(pos, util); getChunkPosAt(pos, util);
Chunk chunk = chunkMap.get(util); Chunk chunk = chunkMap.get(util);
if (chunk == null) return 0; if (chunk == null) return 0;
util.x = (int) Math.floor(pos.x - util.x * Chunk.SIZE); getLocalPosAt(pos.x, pos.y, pos.z, util);
util.y = (int) Math.floor(pos.y - util.y * Chunk.SIZE);
util.z = (int) Math.floor(pos.z - util.z * Chunk.SIZE);
return chunk.getBlockAt(util); return chunk.getBlockAt(util);
} }
@ -70,11 +68,7 @@ public class World {
Vector3i chunkPos = getChunkPosAt(pos); Vector3i chunkPos = getChunkPosAt(pos);
Chunk chunk = chunkMap.get(chunkPos); Chunk chunk = chunkMap.get(chunkPos);
if (chunk == null) return; if (chunk == null) return;
Vector3i blockPos = new Vector3i( Vector3i blockPos = getLocalPosAt(pos.x, pos.y, pos.z, chunkPos);
(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)
);
chunk.setBlockAt(blockPos.x, blockPos.y, blockPos.z, block); chunk.setBlockAt(blockPos.x, blockPos.y, blockPos.z, block);
} }
@ -86,6 +80,10 @@ public class World {
return chunkMap.get(chunkPos); 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. * 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 * 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. * Gets the chunk position at the specified world position.
* @param worldPos The world position. * @param x The x coordinate.
* @return The chunk position. Note that this may not correspond to any existing chunk. * @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) { public static Vector3i getChunkPosAt(Vector3f worldPos) {
return getChunkPosAt(worldPos, new Vector3i()); return getChunkPosAt(worldPos, new Vector3i());
} }
public static Vector3i getChunkPosAt(Vector3f worldPos, Vector3i dest) { public static Vector3i getChunkPosAt(Vector3i worldPos) {
dest.x = (int) Math.floor(worldPos.x / Chunk.SIZE); return getChunkPosAt(worldPos.x, worldPos.y, worldPos.z, new Vector3i());
dest.y = (int) Math.floor(worldPos.y / Chunk.SIZE); }
dest.z = (int) Math.floor(worldPos.z / Chunk.SIZE);
/**
* 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; return dest;
} }
public static Vector3i getLocalPosAt(Vector3i worldPos) {
return getLocalPosAt(worldPos.x, worldPos.y, worldPos.z, new Vector3i());
}
} }

View File

@ -3,16 +3,21 @@ package nl.andrewl.aos_core.net.udp;
import nl.andrewl.record_net.Message; 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. * changes.
*/ */
public record ClientInputState( public record ClientInputState(
int clientId, int clientId,
// Movement
boolean forward, boolean forward,
boolean backward, boolean backward,
boolean left, boolean left,
boolean right, boolean right,
boolean jumping, boolean jumping,
boolean crouching, boolean crouching,
boolean sprinting boolean sprinting,
// Interaction
boolean hitting, // Usually a "left-click" action.
boolean interacting // Usually a "right-click" action.
) implements Message {} ) implements Message {}

View File

@ -1,11 +1,14 @@
package nl.andrewl.aos2_server; 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.Player;
import nl.andrewl.aos_core.model.World; import nl.andrewl.aos_core.model.World;
import nl.andrewl.aos_core.net.udp.ChunkUpdateMessage;
import nl.andrewl.aos_core.net.udp.ClientInputState; import nl.andrewl.aos_core.net.udp.ClientInputState;
import org.joml.Math; import org.joml.Math;
import org.joml.Vector2i; import org.joml.Vector2i;
import org.joml.Vector3f; import org.joml.Vector3f;
import org.joml.Vector3i;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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 = 1.8f;
public static final float HEIGHT_CROUCH = 1.4f; 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 WIDTH = 0.75f;
public static final float RADIUS = WIDTH / 2f; 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 MOVEMENT_DECELERATION = 2f;
public static final float JUMP_SPEED = 8f; public static final float JUMP_SPEED = 8f;
public static final int BLOCK_REMOVE_COOLDOWN = 250;
private ClientInputState lastInputState; private ClientInputState lastInputState;
private long lastBlockRemovedAt = 0;
private boolean updated = false; private boolean updated = false;
public ServerPlayer(int id, String username) { public ServerPlayer(int id, String username) {
super(id, username); super(id, username);
// Initialize with a default state of no input. // 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() { public ClientInputState getLastInputState() {
@ -49,13 +58,36 @@ public class ServerPlayer extends Player {
return updated; 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. 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)) { if (isGrounded(world)) {
tickHorizontalVelocity();
if (lastInputState.jumping()) { if (lastInputState.jumping()) {
velocity.y = JUMP_SPEED; velocity.y = JUMP_SPEED * (lastInputState.sprinting() ? 1.25f : 1f);
updated = true; updated = true;
} }
} else { } else {
@ -73,7 +105,7 @@ public class ServerPlayer extends Player {
} }
} }
private void tickHorizontalVelocity() { private void tickHorizontalVelocity(boolean doDeceleration) {
Vector3f horizontalVelocity = new Vector3f( Vector3f horizontalVelocity = new Vector3f(
velocity.x == velocity.x ? velocity.x : 0f, velocity.x == velocity.x ? velocity.x : 0f,
0, 0,
@ -101,7 +133,7 @@ public class ServerPlayer extends Player {
horizontalVelocity.normalize(maxSpeed); horizontalVelocity.normalize(maxSpeed);
} }
updated = true; updated = true;
} else if (horizontalVelocity.lengthSquared() > 0) { } else if (doDeceleration && horizontalVelocity.lengthSquared() > 0) {
Vector3f deceleration = new Vector3f(horizontalVelocity) Vector3f deceleration = new Vector3f(horizontalVelocity)
.negate().normalize() .negate().normalize()
.mul(Math.min(horizontalVelocity.length(), MOVEMENT_DECELERATION)); .mul(Math.min(horizontalVelocity.length(), MOVEMENT_DECELERATION));
@ -154,105 +186,144 @@ public class ServerPlayer extends Player {
// movement.x, movement.y, movement.z, // movement.x, movement.y, movement.z,
// nextTickPosition.x, nextTickPosition.y, nextTickPosition.z // nextTickPosition.x, nextTickPosition.y, nextTickPosition.z
// ); // );
checkWallCollision(world, nextTickPosition, movement); float height = getCurrentHeight();
checkCeilingCollision(world, nextTickPosition, movement); float delta = 0.00001f;
checkFloorCollision(world, nextTickPosition, movement); 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) { // Check if we collide with anything at this new position.
// 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); float playerBodyPrevMinZ = lastPos.z - RADIUS;
int steps = (int) Math.ceil(dropHeight); float playerBodyPrevMaxZ = lastPos.z + RADIUS;
// System.out.printf(" dropHeight=%.3f, steps=%d%n", dropHeight, steps); float playerBodyPrevMinX = lastPos.x - RADIUS;
// Get a vector describing how much we move for each 1 unit Y decreases. float playerBodyPrevMaxX = lastPos.x + RADIUS;
Vector3f stepSize = new Vector3f(movement).div(dropHeight); float playerBodyPrevMinY = lastPos.y;
Vector3f potentialPosition = new Vector3f(position); float playerBodyPrevMaxY = lastPos.y + height;
for (int i = 0; i < steps; i++) {
potentialPosition.add(stepSize); float playerBodyMinZ = nextPos.z - RADIUS;
// System.out.printf(" Checking: %.3f, %.3f, %.3f%n", potentialPosition.x, potentialPosition.y, potentialPosition.z); float playerBodyMaxZ = nextPos.z + RADIUS;
if (getHorizontalSpaceOccupied(potentialPosition).stream() float playerBodyMinX = nextPos.x - RADIUS;
.anyMatch(p -> world.getBlockAt(p.x, potentialPosition.y, p.y) != 0)) { float playerBodyMaxX = nextPos.x + RADIUS;
// System.out.println(" Occupied!"); float playerBodyMinY = nextPos.y;
position.y = Math.ceil(potentialPosition.y); float playerBodyMaxY = nextPos.y + height;
velocity.y = 0;
movement.y = 0; // Compute the bounds of all blocks the player is intersecting with.
updated = true; int minX = (int) Math.floor(playerBodyMinX);
return; // Exit before doing any extra work. 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) { public float getCurrentHeight() {
// If the player is moving down, or not moving out of their current y level, no point in checking. return lastInputState.crouching() ? HEIGHT_CROUCH : HEIGHT;
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.
}
}
} }
private void checkWallCollision(World world, Vector3f nextTickPosition, Vector3f movement) { public float getEyeHeight() {
// If the player isn't moving horizontally, no point in checking. return lastInputState.crouching() ? EYE_HEIGHT_CROUCH : EYE_HEIGHT;
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;
}
}
} }
} }

View File

@ -56,7 +56,7 @@ public class WorldUpdater implements Runnable {
private void tick() { private void tick() {
for (var player : server.getPlayerManager().getPlayers()) { 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( if (player.isUpdated()) server.getPlayerManager().broadcastUdpMessage(new PlayerUpdateMessage(
player.getId(), player.getId(),
player.getPosition().x, player.getPosition().y, player.getPosition().z, player.getPosition().x, player.getPosition().y, player.getPosition().z,