diff --git a/client/src/main/java/nl/andrewl/aos2_client/Camera.java b/client/src/main/java/nl/andrewl/aos2_client/Camera.java index 80464c8..0a1b6b2 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/Camera.java +++ b/client/src/main/java/nl/andrewl/aos2_client/Camera.java @@ -1,18 +1,14 @@ package nl.andrewl.aos2_client; import nl.andrewl.aos_core.MathUtils; -import nl.andrewl.aos_core.net.udp.ClientOrientationState; import org.joml.Matrix4f; import org.joml.Vector2f; import org.joml.Vector3f; -import org.lwjgl.glfw.GLFWCursorPosCallbackI; - -import static org.lwjgl.glfw.GLFW.glfwGetCursorPos; /** * Represents the player camera in the game world. */ -public class Camera implements GLFWCursorPosCallbackI { +public class Camera { public static final Vector3f UP = new Vector3f(0, 1, 0); public static final Vector3f DOWN = new Vector3f(0, -1, 0); public static final Vector3f RIGHT = new Vector3f(1, 0, 0); @@ -20,13 +16,13 @@ public class Camera implements GLFWCursorPosCallbackI { public static final Vector3f FORWARD = new Vector3f(0, 0, -1); public static final Vector3f BACKWARD = new Vector3f(0, 0, 1); - private final Client client; - /** * The x, y, and z position of the camera in the world. */ private final Vector3f position; + private final Vector3f velocity; + /** * The camera's angular orientation. X refers to the rotation about the * vertical axis, while Y refers to the rotation about the horizontal axis. @@ -43,13 +39,9 @@ public class Camera implements GLFWCursorPosCallbackI { private final Matrix4f viewTransform; private final float[] viewTransformData = new float[16]; - private float lastMouseCursorX; - private float lastMouseCursorY; - private float mouseCursorSensitivity = 0.005f; - - public Camera(Client client) { - this.client = client; + public Camera() { this.position = new Vector3f(); + this.velocity = new Vector3f(); this.orientation = new Vector2f(0, (float) (Math.PI / 2)); this.viewTransform = new Matrix4f(); } @@ -66,14 +58,25 @@ public class Camera implements GLFWCursorPosCallbackI { return orientation; } + public Vector3f getPosition() { + return position; + } + + public Vector3f getVelocity() { + return velocity; + } + public void setPosition(float x, float y, float z) { if (position.x != x || position.y != y || position.z != z) { position.set(x, y, z); updateViewTransform(); - System.out.printf("Position: x=%.2f, y=%.2f, z=%.2f%n", position.x, position.y, position.z); } } + public void setVelocity(float x, float y, float z) { + velocity.set(x, y, z); + } + public void setOrientation(float x, float y) { orientation.set( MathUtils.normalize(x, 0, Math.PI * 2), @@ -94,24 +97,6 @@ public class Camera implements GLFWCursorPosCallbackI { viewTransform.get(viewTransformData); } - @Override - public void invoke(long windowHandle, double xPos, double yPos) { - double[] xb = new double[1]; - double[] yb = new double[1]; - glfwGetCursorPos(windowHandle, xb, yb); - float x = (float) xb[0]; - float y = (float) yb[0]; - float dx = x - lastMouseCursorX; - float dy = y - lastMouseCursorY; - lastMouseCursorX = x; - lastMouseCursorY = y; - setOrientation(orientation.x - dx * mouseCursorSensitivity, orientation.y - dy * mouseCursorSensitivity); - client.getCommunicationHandler().sendDatagramPacket(new ClientOrientationState(client.getClientId(), orientation.x, orientation.y)); -// System.out.printf("rX=%.0f deg about the Y axis, rY=%.0f deg about the X axis%n", Math.toDegrees(orientation.x), Math.toDegrees(orientation.y)); - var vv = getViewVector(); -// System.out.printf("View vector: [%.2f, %.2f, %.2f]%n", vv.x, vv.y, vv.z); - } - public Vector3f getViewVector() { float y = (float) (orientation.y + Math.PI / 2); return new Vector3f( 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 654373b..719fec7 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/Client.java +++ b/client/src/main/java/nl/andrewl/aos2_client/Client.java @@ -1,104 +1,71 @@ package nl.andrewl.aos2_client; -import nl.andrewl.aos2_client.render.ChunkMesh; -import nl.andrewl.aos2_client.render.ChunkMeshGenerator; -import nl.andrewl.aos2_client.render.ChunkRenderer; -import nl.andrewl.aos2_client.render.WindowUtils; +import nl.andrewl.aos2_client.control.PlayerInputKeyCallback; +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.World; -import nl.andrewl.aos_core.net.udp.ClientInputState; +import nl.andrewl.aos_core.net.ChunkDataMessage; +import nl.andrewl.aos_core.net.udp.PlayerUpdateMessage; +import nl.andrewl.record_net.Message; +import org.joml.Vector3f; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.InetAddress; -import static org.lwjgl.glfw.GLFW.*; -import static org.lwjgl.opengl.GL46.*; - public class Client implements Runnable { - public static void main(String[] args) throws IOException { - InetAddress serverAddress = InetAddress.getByName(args[0]); - int serverPort = Integer.parseInt(args[1]); - String username = args[2].trim(); - - Client client = new Client(serverAddress, serverPort, username); - client.run(); - } + private static final Logger log = LoggerFactory.getLogger(Client.class); + public static final double FPS = 60; private final InetAddress serverAddress; private final int serverPort; private final String username; - private final CommunicationHandler communicationHandler; - private ChunkRenderer chunkRenderer; - private int clientId; - private World world; - private Camera cam; + private final CommunicationHandler communicationHandler; + private final GameRenderer gameRenderer; + + private int clientId; + private final World world; public Client(InetAddress serverAddress, int serverPort, String username) { this.serverAddress = serverAddress; this.serverPort = serverPort; this.username = username; this.communicationHandler = new CommunicationHandler(this); + this.gameRenderer = new GameRenderer(); this.world = new World(); - this.cam = new Camera(this); } @Override public void run() { - var windowInfo = WindowUtils.initUI(); - long windowHandle = windowInfo.windowHandle(); - chunkRenderer = new ChunkRenderer(windowInfo.width(), windowInfo.height()); - ChunkMeshGenerator meshGenerator = new ChunkMeshGenerator(); - try { + log.debug("Connecting to server at {}, port {}, with username \"{}\"...", serverAddress, serverPort, username); this.clientId = communicationHandler.establishConnection(serverAddress, serverPort, username); - System.out.println("Established connection to the server."); + log.info("Established a connection to the server."); } catch (IOException e) { - e.printStackTrace(); - return; // Exit without starting the game. + log.error("Couldn't connect to the server: {}", e.getMessage()); + return; } - System.out.println("Waiting for all world data to arrive..."); - try { - Thread.sleep(2000); - } catch (InterruptedException e) { - e.printStackTrace(); + gameRenderer.setupWindow( + new PlayerViewCursorCallback(gameRenderer.getCamera(), communicationHandler), + new PlayerInputKeyCallback(communicationHandler) + ); + + long lastFrameAt = System.currentTimeMillis(); + while (!gameRenderer.windowShouldClose()) { + long now = System.currentTimeMillis(); + float dt = (now - lastFrameAt) / 1000f; + gameRenderer.draw(); + // Interpolate camera movement to make the game feel smooth. + Vector3f camMovement = new Vector3f(gameRenderer.getCamera().getVelocity()).mul(dt); + gameRenderer.getCamera().getPosition().add(camMovement); + lastFrameAt = now; } - for (var chunk : world.getChunkMap().values()) { - chunkRenderer.addChunkMesh(new ChunkMesh(chunk, meshGenerator)); - } - - glfwSetCursorPosCallback(windowHandle, cam); - - ClientInputState lastInputState = null; - - while (!glfwWindowShouldClose(windowHandle)) { - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - - chunkRenderer.draw(cam); - - glfwSwapBuffers(windowHandle); - glfwPollEvents(); - - ClientInputState inputState = new ClientInputState( - clientId, - glfwGetKey(windowHandle, GLFW_KEY_W) == GLFW_PRESS, - glfwGetKey(windowHandle, GLFW_KEY_S) == GLFW_PRESS, - glfwGetKey(windowHandle, GLFW_KEY_A) == GLFW_PRESS, - glfwGetKey(windowHandle, GLFW_KEY_D) == GLFW_PRESS, - glfwGetKey(windowHandle, GLFW_KEY_SPACE) == GLFW_PRESS, - glfwGetKey(windowHandle, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS, - false - ); - if (!inputState.equals(lastInputState)) { - communicationHandler.sendDatagramPacket(inputState); - lastInputState = inputState; - } - } - + gameRenderer.freeWindow(); communicationHandler.shutdown(); - - chunkRenderer.free(); - WindowUtils.clearUI(windowHandle); } public int getClientId() { @@ -109,15 +76,27 @@ public class Client implements Runnable { return world; } - public Camera getCam() { - return cam; + public void onMessageReceived(Message msg) { + if (msg instanceof ChunkDataMessage chunkDataMessage) { + Chunk chunk = chunkDataMessage.toChunk(); + world.addChunk(chunk); + gameRenderer.getChunkRenderer().addChunkMesh(chunk); + } + if (msg instanceof PlayerUpdateMessage playerUpdate) { + if (playerUpdate.clientId() == clientId) { + gameRenderer.getCamera().setPosition(playerUpdate.px(), playerUpdate.py() + 1.8f, playerUpdate.pz()); + gameRenderer.getCamera().setVelocity(playerUpdate.vx(), playerUpdate.vy(), playerUpdate.vz()); + } + } } - public CommunicationHandler getCommunicationHandler() { - return communicationHandler; - } - public ChunkRenderer getChunkRenderer() { - return chunkRenderer; + public static void main(String[] args) throws IOException { + InetAddress serverAddress = InetAddress.getByName(args[0]); + int serverPort = Integer.parseInt(args[1]); + String username = args[2].trim(); + + Client client = new Client(serverAddress, serverPort, username); + client.run(); } } diff --git a/client/src/main/java/nl/andrewl/aos2_client/ClientWorld.java b/client/src/main/java/nl/andrewl/aos2_client/ClientWorld.java new file mode 100644 index 0000000..b434ada --- /dev/null +++ b/client/src/main/java/nl/andrewl/aos2_client/ClientWorld.java @@ -0,0 +1,7 @@ +package nl.andrewl.aos2_client; + +import nl.andrewl.aos_core.model.World; + +public class ClientWorld extends World { + +} diff --git a/client/src/main/java/nl/andrewl/aos2_client/CommunicationHandler.java b/client/src/main/java/nl/andrewl/aos2_client/CommunicationHandler.java index 1113646..9ae53d6 100644 --- a/client/src/main/java/nl/andrewl/aos2_client/CommunicationHandler.java +++ b/client/src/main/java/nl/andrewl/aos2_client/CommunicationHandler.java @@ -1,10 +1,8 @@ package nl.andrewl.aos2_client; import nl.andrewl.aos_core.Net; -import nl.andrewl.aos_core.model.Chunk; import nl.andrewl.aos_core.net.*; import nl.andrewl.aos_core.net.udp.DatagramInit; -import nl.andrewl.aos_core.net.udp.PlayerUpdateMessage; import nl.andrewl.record_net.Message; import nl.andrewl.record_net.util.ExtendedDataInputStream; import nl.andrewl.record_net.util.ExtendedDataOutputStream; @@ -36,7 +34,6 @@ public class CommunicationHandler { } public int establishConnection(InetAddress address, int port, String username) throws IOException { - log.debug("Connecting to server at {}, port {}, with username \"{}\"...", address, port, username); if (socket != null && !socket.isClosed()) { socket.close(); } @@ -53,8 +50,8 @@ public class CommunicationHandler { if (response instanceof ConnectAcceptMessage acceptMessage) { this.clientId = acceptMessage.clientId(); establishDatagramConnection(); - new Thread(new TcpReceiver(in, this::handleMessage)).start(); - new Thread(new UdpReceiver(datagramSocket, this::handleUdpMessage)).start(); + new Thread(new TcpReceiver(in, client::onMessageReceived)).start(); + new Thread(new UdpReceiver(datagramSocket, (msg, packet) -> client.onMessageReceived(msg))).start(); return acceptMessage.clientId(); } else { throw new IOException("Server returned an unexpected message: " + response); @@ -117,19 +114,7 @@ public class CommunicationHandler { log.debug("Established datagram communication with the server."); } - private void handleMessage(Message msg) { - if (msg instanceof ChunkDataMessage chunkDataMessage) { - Chunk chunk = chunkDataMessage.toChunk(); - client.getWorld().addChunk(chunk); - } - } - - private void handleUdpMessage(Message msg, DatagramPacket packet) { - if (msg instanceof PlayerUpdateMessage playerUpdate) { -// log.debug("Received player update: {}", playerUpdate); - if (playerUpdate.clientId() == client.getClientId()) { - client.getCam().setPosition(playerUpdate.px(), playerUpdate.py() + 1.8f, playerUpdate.pz()); - } - } + public int getClientId() { + return clientId; } } 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 new file mode 100644 index 0000000..9e74c94 --- /dev/null +++ b/client/src/main/java/nl/andrewl/aos2_client/control/PlayerInputKeyCallback.java @@ -0,0 +1,38 @@ +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; + + public PlayerInputKeyCallback(CommunicationHandler comm) { + this.comm = comm; + } + + @Override + public void invoke(long window, int key, int scancode, int action, int mods) { + 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; + } + } +} 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 new file mode 100644 index 0000000..e1fffda --- /dev/null +++ b/client/src/main/java/nl/andrewl/aos2_client/control/PlayerViewCursorCallback.java @@ -0,0 +1,47 @@ +package nl.andrewl.aos2_client.control; + +import nl.andrewl.aos2_client.Camera; +import nl.andrewl.aos2_client.CommunicationHandler; +import nl.andrewl.aos_core.net.udp.ClientOrientationState; +import org.lwjgl.glfw.GLFWCursorPosCallbackI; + +import static org.lwjgl.glfw.GLFW.glfwGetCursorPos; + +public class PlayerViewCursorCallback implements GLFWCursorPosCallbackI { + /** + * The number of milliseconds to wait before sending orientation updates, + * to prevent overloading the server. + */ + private static final int ORIENTATION_UPDATE_LIMIT = 20; + + private final Camera camera; + private final CommunicationHandler comm; + private float lastMouseCursorX; + private float lastMouseCursorY; + private float mouseCursorSensitivity = 0.005f; + private long lastOrientationUpdateSentAt = 0L; + + public PlayerViewCursorCallback(Camera camera, CommunicationHandler comm) { + this.camera = camera; + this.comm = comm; + } + + @Override + public void invoke(long window, double xpos, double ypos) { + double[] xb = new double[1]; + double[] yb = new double[1]; + glfwGetCursorPos(window, xb, yb); + float x = (float) xb[0]; + float y = (float) yb[0]; + float dx = x - lastMouseCursorX; + float dy = y - lastMouseCursorY; + lastMouseCursorX = x; + lastMouseCursorY = y; + camera.setOrientation(camera.getOrientation().x - dx * mouseCursorSensitivity, camera.getOrientation().y - dy * mouseCursorSensitivity); + long now = System.currentTimeMillis(); + if (lastOrientationUpdateSentAt + ORIENTATION_UPDATE_LIMIT < now) { + comm.sendDatagramPacket(new ClientOrientationState(comm.getClientId(), camera.getOrientation().x, camera.getOrientation().y)); + lastOrientationUpdateSentAt = now; + } + } +} 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 8039a25..10d1cf4 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 @@ -6,6 +6,8 @@ import org.joml.Matrix4f; import java.util.ArrayList; import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; import static org.lwjgl.opengl.GL46.*; @@ -15,18 +17,17 @@ import static org.lwjgl.opengl.GL46.*; * be rendered each frame. */ public class ChunkRenderer { - private final ShaderProgram shaderProgram; - private final int projectionTransformUniform; - private final int viewTransformUniform; - private final int chunkPositionUniform; - private final int chunkSizeUniform; + private final ChunkMeshGenerator chunkMeshGenerator = new ChunkMeshGenerator(); + private final Queue meshGenerationQueue = new ConcurrentLinkedQueue<>(); - private final Matrix4f projectionTransform; + private ShaderProgram shaderProgram; + private int projectionTransformUniform; + private int viewTransformUniform; + private int chunkPositionUniform; private final List chunkMeshes = new ArrayList<>(); - public ChunkRenderer(int windowWidth, int windowHeight) { - this.projectionTransform = new Matrix4f().perspective(70, (float) windowWidth / (float) windowHeight, 0.01f, 500.0f); + public void setupShaderProgram() { this.shaderProgram = new ShaderProgram.Builder() .withShader("shader/chunk/vertex.glsl", GL_VERTEX_SHADER) .withShader("shader/chunk/fragment.glsl", GL_FRAGMENT_SHADER) @@ -35,18 +36,24 @@ public class ChunkRenderer { this.projectionTransformUniform = shaderProgram.getUniform("projectionTransform"); this.viewTransformUniform = shaderProgram.getUniform("viewTransform"); this.chunkPositionUniform = shaderProgram.getUniform("chunkPosition"); - this.chunkSizeUniform = shaderProgram.getUniform("chunkSize"); + int chunkSizeUniform = shaderProgram.getUniform("chunkSize"); - // Preemptively load projection transform, which doesn't change much. - glUniformMatrix4fv(projectionTransformUniform, false, projectionTransform.get(new float[16])); + // Set constant uniforms that don't change during runtime. glUniform1i(chunkSizeUniform, Chunk.SIZE); } - public void addChunkMesh(ChunkMesh mesh) { - this.chunkMeshes.add(mesh); + public void addChunkMesh(Chunk chunk) { + meshGenerationQueue.add(chunk); + } + + public void setPerspective(Matrix4f projectionTransform) { + glUniformMatrix4fv(projectionTransformUniform, false, projectionTransform.get(new float[16])); } public void draw(Camera cam) { + while (!meshGenerationQueue.isEmpty()) { + chunkMeshes.add(new ChunkMesh(meshGenerationQueue.remove(), chunkMeshGenerator)); + } shaderProgram.use(); glUniformMatrix4fv(viewTransformUniform, false, cam.getViewTransformData()); for (var mesh : chunkMeshes) { 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 new file mode 100644 index 0000000..1a025cd --- /dev/null +++ b/client/src/main/java/nl/andrewl/aos2_client/render/GameRenderer.java @@ -0,0 +1,157 @@ +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 org.joml.Matrix4f; +import org.lwjgl.glfw.Callbacks; +import org.lwjgl.glfw.GLFWErrorCallback; +import org.lwjgl.glfw.GLFWVidMode; +import org.lwjgl.opengl.GL; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.lwjgl.glfw.GLFW.*; +import static org.lwjgl.opengl.GL46.*; + +/** + * This component manages all the view-related aspects of the client, such as + * chunk rendering, window setup and removal, and other OpenGL functions. It + * should generally only be invoked on the main thread, since this is where the + * OpenGL context exists. + */ +public class GameRenderer { + private static final Logger log = LoggerFactory.getLogger(GameRenderer.class); + private static final float Z_NEAR = 0.01f; + private static final float Z_FAR = 500f; + + private final ChunkRenderer chunkRenderer; + private final Camera camera; + + private long windowHandle; + private GLFWVidMode primaryMonitorSettings; + private boolean fullscreen; + private int screenWidth = 800; + private int screenHeight = 600; + private float fov = 70f; + + private final Matrix4f perspectiveTransform; + + public GameRenderer() { + this.chunkRenderer = new ChunkRenderer(); + this.camera = new Camera(); + this.perspectiveTransform = new Matrix4f(); + + } + + public void setupWindow(PlayerViewCursorCallback viewCursorCallback, PlayerInputKeyCallback inputKeyCallback) { + GLFWErrorCallback.createPrint(System.err).set(); + if (!glfwInit()) throw new IllegalStateException("Could not initialize GLFW."); + glfwDefaultWindowHints(); + glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); + glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); + + primaryMonitorSettings = glfwGetVideoMode(glfwGetPrimaryMonitor()); + if (primaryMonitorSettings == null) throw new IllegalStateException("Could not get information about the primary monitory."); + windowHandle = glfwCreateWindow(screenWidth, screenHeight, "Ace of Shades 2", 0, 0); + if (windowHandle == 0) throw new RuntimeException("Failed to create GLFW window."); + fullscreen = false; + log.debug("Initialized GLFW window."); + + // Setup callbacks. + glfwSetKeyCallback(windowHandle, inputKeyCallback); + glfwSetCursorPosCallback(windowHandle, viewCursorCallback); + glfwSetInputMode(windowHandle, GLFW_CURSOR, GLFW_CURSOR_DISABLED); + glfwSetInputMode(windowHandle, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE); + glfwSetCursorPos(windowHandle, 0, 0); + log.debug("Set up window callbacks."); + + glfwMakeContextCurrent(windowHandle); + glfwSwapInterval(1); + glfwShowWindow(windowHandle); + log.debug("Made window visible."); + + GL.createCapabilities(); +// GLUtil.setupDebugMessageCallback(System.out); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glEnable(GL_CULL_FACE); + glEnable(GL_DEPTH_TEST); + glCullFace(GL_BACK); + log.debug("Initialized OpenGL context."); + + chunkRenderer.setupShaderProgram(); + updatePerspective(); + } + + public void setFullscreen(boolean fullscreen) { + if (windowHandle == 0) throw new IllegalStateException("Window not setup."); + long monitor = glfwGetPrimaryMonitor(); + if (!this.fullscreen && fullscreen) { + glfwSetWindowMonitor(windowHandle, monitor, 0, 0, primaryMonitorSettings.width(), primaryMonitorSettings.height(), primaryMonitorSettings.refreshRate()); + screenWidth = primaryMonitorSettings.width(); + screenHeight = primaryMonitorSettings.height(); + updatePerspective(); + } else if (this.fullscreen && !fullscreen) { + screenWidth = 800; + screenHeight = 600; + int left = primaryMonitorSettings.width() / 2; + int top = primaryMonitorSettings.height() / 2; + glfwSetWindowMonitor(windowHandle, 0, left, top, screenWidth, screenHeight, primaryMonitorSettings.refreshRate()); + updatePerspective(); + } + this.fullscreen = fullscreen; + } + + public void setSize(int width, int height) { + glfwSetWindowSize(windowHandle, width, height); + this.screenWidth = width; + this.screenHeight = height; + updatePerspective(); + } + + public void setFov(float fov) { + this.fov = fov; + updatePerspective(); + } + + /** + * 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); + chunkRenderer.setPerspective(perspectiveTransform); + } + + public boolean windowShouldClose() { + return glfwWindowShouldClose(windowHandle); + } + + public Camera getCamera() { + return camera; + } + + public ChunkRenderer getChunkRenderer() { + return chunkRenderer; + } + + public void draw() { + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + chunkRenderer.draw(camera); + + glfwSwapBuffers(windowHandle); + glfwPollEvents(); + } + + public void freeWindow() { + chunkRenderer.free(); + GL.destroy(); + Callbacks.glfwFreeCallbacks(windowHandle); + glfwSetErrorCallback(null); + glfwDestroyWindow(windowHandle); + glfwTerminate(); + } +} diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/WindowInfo.java b/client/src/main/java/nl/andrewl/aos2_client/render/WindowInfo.java deleted file mode 100644 index 6c56a8c..0000000 --- a/client/src/main/java/nl/andrewl/aos2_client/render/WindowInfo.java +++ /dev/null @@ -1,7 +0,0 @@ -package nl.andrewl.aos2_client.render; - -public record WindowInfo( - long windowHandle, - int width, - int height -) {} diff --git a/client/src/main/java/nl/andrewl/aos2_client/render/WindowUtils.java b/client/src/main/java/nl/andrewl/aos2_client/render/WindowUtils.java deleted file mode 100644 index 49d3db5..0000000 --- a/client/src/main/java/nl/andrewl/aos2_client/render/WindowUtils.java +++ /dev/null @@ -1,58 +0,0 @@ -package nl.andrewl.aos2_client.render; - -import org.lwjgl.glfw.Callbacks; -import org.lwjgl.glfw.GLFWErrorCallback; -import org.lwjgl.opengl.GL; - -import static org.lwjgl.glfw.GLFW.*; -import static org.lwjgl.opengl.GL11.*; - -public class WindowUtils { - public static WindowInfo initUI() { - GLFWErrorCallback.createPrint(System.err).set(); - if (!glfwInit()) throw new IllegalStateException("Could not initialize GLFW."); - glfwDefaultWindowHints(); - glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); - glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); - - var vidMode = glfwGetVideoMode(glfwGetPrimaryMonitor()); - if (vidMode == null) throw new IllegalStateException("Could not get information about the primary monitory."); - int width = vidMode.width(); - int height = vidMode.height(); - width = 800; height = 600; - long windowHandle = glfwCreateWindow(width, height, "Ace of Shades 2", 0, 0); - if (windowHandle == 0) throw new RuntimeException("Failed to create GLFW window."); - - glfwSetKeyCallback(windowHandle, (window, key, scancode, action, mods) -> { - if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE) { - glfwSetWindowShouldClose(windowHandle, true); - } - }); - - glfwSetInputMode(windowHandle, GLFW_CURSOR, GLFW_CURSOR_DISABLED); - glfwSetInputMode(windowHandle, GLFW_RAW_MOUSE_MOTION, GLFW_TRUE); - - glfwSetWindowPos(windowHandle, 0, 0); - glfwSetCursorPos(windowHandle, 0, 0); - - glfwMakeContextCurrent(windowHandle); - glfwSwapInterval(1); - glfwShowWindow(windowHandle); - - GL.createCapabilities(); -// GLUtil.setupDebugMessageCallback(System.out); - glClearColor(0.0f, 0.0f, 0.0f, 0.0f); - glEnable(GL_CULL_FACE); - glEnable(GL_DEPTH_TEST); - glCullFace(GL_BACK); - - return new WindowInfo(windowHandle, width, height); - } - - public static void clearUI(long windowHandle) { - Callbacks.glfwFreeCallbacks(windowHandle); - glfwDestroyWindow(windowHandle); - glfwTerminate(); - glfwSetErrorCallback(null).free(); - } -} diff --git a/core/src/main/java/nl/andrewl/aos_core/model/World.java b/core/src/main/java/nl/andrewl/aos_core/model/World.java index 987bea4..d97063b 100644 --- a/core/src/main/java/nl/andrewl/aos_core/model/World.java +++ b/core/src/main/java/nl/andrewl/aos_core/model/World.java @@ -13,7 +13,7 @@ import java.util.Map; * that players can interact in. */ public class World { - private final Map chunkMap = new HashMap<>(); + protected final Map chunkMap = new HashMap<>(); public void addChunk(Chunk chunk) { chunkMap.put(chunk.getPosition(), chunk); 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 cbcfb6c..d53b38b 100644 --- a/server/src/main/java/nl/andrewl/aos2_server/WorldUpdater.java +++ b/server/src/main/java/nl/andrewl/aos2_server/WorldUpdater.java @@ -1,6 +1,5 @@ package nl.andrewl.aos2_server; -import nl.andrewl.aos_core.model.World; import nl.andrewl.aos_core.net.udp.PlayerUpdateMessage; import org.joml.Math; import org.joml.Matrix4f; @@ -18,11 +17,13 @@ public class WorldUpdater implements Runnable { private final Server server; private final float ticksPerSecond; + private final float secondsPerTick; private volatile boolean running; public WorldUpdater(Server server, float ticksPerSecond) { this.server = server; this.ticksPerSecond = ticksPerSecond; + this.secondsPerTick = 1.0f / ticksPerSecond; } public void shutdown() { @@ -66,7 +67,7 @@ public class WorldUpdater implements Runnable { var p = player.getPosition(); // Check if we have a negative velocity that will cause us to fall through a block next tick. - float nextTickY = p.y + v.y; + float nextTickY = p.y + v.y * secondsPerTick; if (server.getWorld().getBlockAt(new Vector3f(p.x, nextTickY, p.z)) != 0) { // Find the first block we'll hit and set the player down on that. int floorY = (int) Math.floor(p.y) - 1; @@ -85,12 +86,12 @@ public class WorldUpdater implements Runnable { boolean grounded = (Math.floor(p.y) == p.y && server.getWorld().getBlockAt(new Vector3f(p.x, p.y - 0.0001f, p.z)) != 0); if (!grounded) { - v.y -= 0.1f; + v.y -= 3f; } // Apply horizontal deceleration to the player before computing any input-derived acceleration. if (grounded && hv.length() > 0) { - Vector3f deceleration = new Vector3f(hv).negate().normalize().mul(0.1f); + Vector3f deceleration = new Vector3f(hv).negate().normalize().mul(Math.min(hv.length(), 2f)); hv.add(deceleration); if (hv.length() < 0.1f) { hv.set(0); @@ -103,9 +104,10 @@ public class WorldUpdater implements Runnable { Vector3f a = new Vector3f(); var inputState = player.getLastInputState(); if (inputState.jumping() && grounded) { - v.y = 0.6f; + v.y = 15f; } + final float horizontalAcceleration = 5; // Compute horizontal motion separately. if (grounded) { if (inputState.forward()) a.z -= 1; @@ -118,9 +120,17 @@ public class WorldUpdater implements Runnable { Matrix4f moveTransform = new Matrix4f(); moveTransform.rotate(player.getOrientation().x, new Vector3f(0, 1, 0)); moveTransform.transformDirection(a); + a.mul(horizontalAcceleration); hv.add(a); - final float maxSpeed = 0.25f; // Blocks per tick. + final float maxSpeed; + if (inputState.crouching()) { + maxSpeed = 2.5f; + } else if (inputState.sprinting()) { + maxSpeed = 10f; + } else { + maxSpeed = 6f; + } if (hv.length() > maxSpeed) { hv.normalize(maxSpeed); } @@ -132,7 +142,9 @@ public class WorldUpdater implements Runnable { // Apply velocity to the player's position. if (v.lengthSquared() > 0) { - p.add(v); + Vector3f scaledVelocity = new Vector3f(v); + scaledVelocity.mul(secondsPerTick); + p.add(scaledVelocity); updated = true; }