Refactored client game rendering logic.

This commit is contained in:
Andrew Lalis 2022-07-08 16:19:19 +02:00
parent aed69f6dfb
commit 9e8574ce21
12 changed files with 366 additions and 214 deletions

View File

@ -1,18 +1,14 @@
package nl.andrewl.aos2_client; package nl.andrewl.aos2_client;
import nl.andrewl.aos_core.MathUtils; import nl.andrewl.aos_core.MathUtils;
import nl.andrewl.aos_core.net.udp.ClientOrientationState;
import org.joml.Matrix4f; import org.joml.Matrix4f;
import org.joml.Vector2f; import org.joml.Vector2f;
import org.joml.Vector3f; import org.joml.Vector3f;
import org.lwjgl.glfw.GLFWCursorPosCallbackI;
import static org.lwjgl.glfw.GLFW.glfwGetCursorPos;
/** /**
* Represents the player camera in the game world. * 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 UP = new Vector3f(0, 1, 0);
public static final Vector3f DOWN = 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); 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 FORWARD = new Vector3f(0, 0, -1);
public static final Vector3f BACKWARD = 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. * The x, y, and z position of the camera in the world.
*/ */
private final Vector3f position; private final Vector3f position;
private final Vector3f velocity;
/** /**
* The camera's angular orientation. X refers to the rotation about the * The camera's angular orientation. X refers to the rotation about the
* vertical axis, while Y refers to the rotation about the horizontal axis. * 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 Matrix4f viewTransform;
private final float[] viewTransformData = new float[16]; private final float[] viewTransformData = new float[16];
private float lastMouseCursorX; public Camera() {
private float lastMouseCursorY;
private float mouseCursorSensitivity = 0.005f;
public Camera(Client client) {
this.client = client;
this.position = new Vector3f(); this.position = new Vector3f();
this.velocity = new Vector3f();
this.orientation = new Vector2f(0, (float) (Math.PI / 2)); this.orientation = new Vector2f(0, (float) (Math.PI / 2));
this.viewTransform = new Matrix4f(); this.viewTransform = new Matrix4f();
} }
@ -66,14 +58,25 @@ public class Camera implements GLFWCursorPosCallbackI {
return orientation; return orientation;
} }
public Vector3f getPosition() {
return position;
}
public Vector3f getVelocity() {
return velocity;
}
public void setPosition(float x, float y, float z) { public void setPosition(float x, float y, float z) {
if (position.x != x || position.y != y || position.z != z) { if (position.x != x || position.y != y || position.z != z) {
position.set(x, y, z); position.set(x, y, z);
updateViewTransform(); 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) { public void setOrientation(float x, float y) {
orientation.set( orientation.set(
MathUtils.normalize(x, 0, Math.PI * 2), MathUtils.normalize(x, 0, Math.PI * 2),
@ -94,24 +97,6 @@ public class Camera implements GLFWCursorPosCallbackI {
viewTransform.get(viewTransformData); 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() { public Vector3f getViewVector() {
float y = (float) (orientation.y + Math.PI / 2); float y = (float) (orientation.y + Math.PI / 2);
return new Vector3f( return new Vector3f(

View File

@ -1,104 +1,71 @@
package nl.andrewl.aos2_client; package nl.andrewl.aos2_client;
import nl.andrewl.aos2_client.render.ChunkMesh; import nl.andrewl.aos2_client.control.PlayerInputKeyCallback;
import nl.andrewl.aos2_client.render.ChunkMeshGenerator; import nl.andrewl.aos2_client.control.PlayerViewCursorCallback;
import nl.andrewl.aos2_client.render.ChunkRenderer; import nl.andrewl.aos2_client.render.GameRenderer;
import nl.andrewl.aos2_client.render.WindowUtils; import nl.andrewl.aos_core.model.Chunk;
import nl.andrewl.aos_core.model.World; 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.io.IOException;
import java.net.InetAddress; import java.net.InetAddress;
import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.opengl.GL46.*;
public class Client implements Runnable { public class Client implements Runnable {
public static void main(String[] args) throws IOException { private static final Logger log = LoggerFactory.getLogger(Client.class);
InetAddress serverAddress = InetAddress.getByName(args[0]); public static final double FPS = 60;
int serverPort = Integer.parseInt(args[1]);
String username = args[2].trim();
Client client = new Client(serverAddress, serverPort, username);
client.run();
}
private final InetAddress serverAddress; private final InetAddress serverAddress;
private final int serverPort; private final int serverPort;
private final String username; private final String username;
private final CommunicationHandler communicationHandler;
private ChunkRenderer chunkRenderer;
private int clientId;
private World world; private final CommunicationHandler communicationHandler;
private Camera cam; private final GameRenderer gameRenderer;
private int clientId;
private final World world;
public Client(InetAddress serverAddress, int serverPort, String username) { public Client(InetAddress serverAddress, int serverPort, String username) {
this.serverAddress = serverAddress; this.serverAddress = serverAddress;
this.serverPort = serverPort; this.serverPort = serverPort;
this.username = username; this.username = username;
this.communicationHandler = new CommunicationHandler(this); this.communicationHandler = new CommunicationHandler(this);
this.gameRenderer = new GameRenderer();
this.world = new World(); this.world = new World();
this.cam = new Camera(this);
} }
@Override @Override
public void run() { public void run() {
var windowInfo = WindowUtils.initUI();
long windowHandle = windowInfo.windowHandle();
chunkRenderer = new ChunkRenderer(windowInfo.width(), windowInfo.height());
ChunkMeshGenerator meshGenerator = new ChunkMeshGenerator();
try { try {
log.debug("Connecting to server at {}, port {}, with username \"{}\"...", serverAddress, serverPort, username);
this.clientId = communicationHandler.establishConnection(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) { } catch (IOException e) {
e.printStackTrace(); log.error("Couldn't connect to the server: {}", e.getMessage());
return; // Exit without starting the game. return;
} }
System.out.println("Waiting for all world data to arrive..."); gameRenderer.setupWindow(
try { new PlayerViewCursorCallback(gameRenderer.getCamera(), communicationHandler),
Thread.sleep(2000); new PlayerInputKeyCallback(communicationHandler)
} catch (InterruptedException e) { );
e.printStackTrace();
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()) { gameRenderer.freeWindow();
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;
}
}
communicationHandler.shutdown(); communicationHandler.shutdown();
chunkRenderer.free();
WindowUtils.clearUI(windowHandle);
} }
public int getClientId() { public int getClientId() {
@ -109,15 +76,27 @@ public class Client implements Runnable {
return world; return world;
} }
public Camera getCam() { public void onMessageReceived(Message msg) {
return cam; 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() { public static void main(String[] args) throws IOException {
return chunkRenderer; 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();
} }
} }

View File

@ -0,0 +1,7 @@
package nl.andrewl.aos2_client;
import nl.andrewl.aos_core.model.World;
public class ClientWorld extends World {
}

View File

@ -1,10 +1,8 @@
package nl.andrewl.aos2_client; package nl.andrewl.aos2_client;
import nl.andrewl.aos_core.Net; 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.*;
import nl.andrewl.aos_core.net.udp.DatagramInit; 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.Message;
import nl.andrewl.record_net.util.ExtendedDataInputStream; import nl.andrewl.record_net.util.ExtendedDataInputStream;
import nl.andrewl.record_net.util.ExtendedDataOutputStream; 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 { 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()) { if (socket != null && !socket.isClosed()) {
socket.close(); socket.close();
} }
@ -53,8 +50,8 @@ public class CommunicationHandler {
if (response instanceof ConnectAcceptMessage acceptMessage) { if (response instanceof ConnectAcceptMessage acceptMessage) {
this.clientId = acceptMessage.clientId(); this.clientId = acceptMessage.clientId();
establishDatagramConnection(); establishDatagramConnection();
new Thread(new TcpReceiver(in, this::handleMessage)).start(); new Thread(new TcpReceiver(in, client::onMessageReceived)).start();
new Thread(new UdpReceiver(datagramSocket, this::handleUdpMessage)).start(); new Thread(new UdpReceiver(datagramSocket, (msg, packet) -> client.onMessageReceived(msg))).start();
return acceptMessage.clientId(); return acceptMessage.clientId();
} else { } else {
throw new IOException("Server returned an unexpected message: " + response); throw new IOException("Server returned an unexpected message: " + response);
@ -117,19 +114,7 @@ public class CommunicationHandler {
log.debug("Established datagram communication with the server."); log.debug("Established datagram communication with the server.");
} }
private void handleMessage(Message msg) { public int getClientId() {
if (msg instanceof ChunkDataMessage chunkDataMessage) { return clientId;
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());
}
}
} }
} }

View File

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

View File

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

View File

@ -6,6 +6,8 @@ import org.joml.Matrix4f;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import static org.lwjgl.opengl.GL46.*; import static org.lwjgl.opengl.GL46.*;
@ -15,18 +17,17 @@ import static org.lwjgl.opengl.GL46.*;
* be rendered each frame. * be rendered each frame.
*/ */
public class ChunkRenderer { public class ChunkRenderer {
private final ShaderProgram shaderProgram; private final ChunkMeshGenerator chunkMeshGenerator = new ChunkMeshGenerator();
private final int projectionTransformUniform; private final Queue<Chunk> meshGenerationQueue = new ConcurrentLinkedQueue<>();
private final int viewTransformUniform;
private final int chunkPositionUniform;
private final int chunkSizeUniform;
private final Matrix4f projectionTransform; private ShaderProgram shaderProgram;
private int projectionTransformUniform;
private int viewTransformUniform;
private int chunkPositionUniform;
private final List<ChunkMesh> chunkMeshes = new ArrayList<>(); private final List<ChunkMesh> chunkMeshes = new ArrayList<>();
public ChunkRenderer(int windowWidth, int windowHeight) { public void setupShaderProgram() {
this.projectionTransform = new Matrix4f().perspective(70, (float) windowWidth / (float) windowHeight, 0.01f, 500.0f);
this.shaderProgram = new ShaderProgram.Builder() this.shaderProgram = new ShaderProgram.Builder()
.withShader("shader/chunk/vertex.glsl", GL_VERTEX_SHADER) .withShader("shader/chunk/vertex.glsl", GL_VERTEX_SHADER)
.withShader("shader/chunk/fragment.glsl", GL_FRAGMENT_SHADER) .withShader("shader/chunk/fragment.glsl", GL_FRAGMENT_SHADER)
@ -35,18 +36,24 @@ public class ChunkRenderer {
this.projectionTransformUniform = shaderProgram.getUniform("projectionTransform"); this.projectionTransformUniform = shaderProgram.getUniform("projectionTransform");
this.viewTransformUniform = shaderProgram.getUniform("viewTransform"); this.viewTransformUniform = shaderProgram.getUniform("viewTransform");
this.chunkPositionUniform = shaderProgram.getUniform("chunkPosition"); this.chunkPositionUniform = shaderProgram.getUniform("chunkPosition");
this.chunkSizeUniform = shaderProgram.getUniform("chunkSize"); int chunkSizeUniform = shaderProgram.getUniform("chunkSize");
// Preemptively load projection transform, which doesn't change much. // Set constant uniforms that don't change during runtime.
glUniformMatrix4fv(projectionTransformUniform, false, projectionTransform.get(new float[16]));
glUniform1i(chunkSizeUniform, Chunk.SIZE); glUniform1i(chunkSizeUniform, Chunk.SIZE);
} }
public void addChunkMesh(ChunkMesh mesh) { public void addChunkMesh(Chunk chunk) {
this.chunkMeshes.add(mesh); meshGenerationQueue.add(chunk);
}
public void setPerspective(Matrix4f projectionTransform) {
glUniformMatrix4fv(projectionTransformUniform, false, projectionTransform.get(new float[16]));
} }
public void draw(Camera cam) { public void draw(Camera cam) {
while (!meshGenerationQueue.isEmpty()) {
chunkMeshes.add(new ChunkMesh(meshGenerationQueue.remove(), chunkMeshGenerator));
}
shaderProgram.use(); shaderProgram.use();
glUniformMatrix4fv(viewTransformUniform, false, cam.getViewTransformData()); glUniformMatrix4fv(viewTransformUniform, false, cam.getViewTransformData());
for (var mesh : chunkMeshes) { for (var mesh : chunkMeshes) {

View File

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

View File

@ -1,7 +0,0 @@
package nl.andrewl.aos2_client.render;
public record WindowInfo(
long windowHandle,
int width,
int height
) {}

View File

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

View File

@ -13,7 +13,7 @@ import java.util.Map;
* that players can interact in. * that players can interact in.
*/ */
public class World { public class World {
private final Map<Vector3ic, Chunk> chunkMap = new HashMap<>(); protected final Map<Vector3ic, Chunk> chunkMap = new HashMap<>();
public void addChunk(Chunk chunk) { public void addChunk(Chunk chunk) {
chunkMap.put(chunk.getPosition(), chunk); chunkMap.put(chunk.getPosition(), chunk);

View File

@ -1,6 +1,5 @@
package nl.andrewl.aos2_server; package nl.andrewl.aos2_server;
import nl.andrewl.aos_core.model.World;
import nl.andrewl.aos_core.net.udp.PlayerUpdateMessage; import nl.andrewl.aos_core.net.udp.PlayerUpdateMessage;
import org.joml.Math; import org.joml.Math;
import org.joml.Matrix4f; import org.joml.Matrix4f;
@ -18,11 +17,13 @@ public class WorldUpdater implements Runnable {
private final Server server; private final Server server;
private final float ticksPerSecond; private final float ticksPerSecond;
private final float secondsPerTick;
private volatile boolean running; private volatile boolean running;
public WorldUpdater(Server server, float ticksPerSecond) { public WorldUpdater(Server server, float ticksPerSecond) {
this.server = server; this.server = server;
this.ticksPerSecond = ticksPerSecond; this.ticksPerSecond = ticksPerSecond;
this.secondsPerTick = 1.0f / ticksPerSecond;
} }
public void shutdown() { public void shutdown() {
@ -66,7 +67,7 @@ public class WorldUpdater implements Runnable {
var p = player.getPosition(); var p = player.getPosition();
// Check if we have a negative velocity that will cause us to fall through a block next tick. // 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) { 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. // Find the first block we'll hit and set the player down on that.
int floorY = (int) Math.floor(p.y) - 1; 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); boolean grounded = (Math.floor(p.y) == p.y && server.getWorld().getBlockAt(new Vector3f(p.x, p.y - 0.0001f, p.z)) != 0);
if (!grounded) { if (!grounded) {
v.y -= 0.1f; v.y -= 3f;
} }
// Apply horizontal deceleration to the player before computing any input-derived acceleration. // Apply horizontal deceleration to the player before computing any input-derived acceleration.
if (grounded && hv.length() > 0) { 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); hv.add(deceleration);
if (hv.length() < 0.1f) { if (hv.length() < 0.1f) {
hv.set(0); hv.set(0);
@ -103,9 +104,10 @@ public class WorldUpdater implements Runnable {
Vector3f a = new Vector3f(); Vector3f a = new Vector3f();
var inputState = player.getLastInputState(); var inputState = player.getLastInputState();
if (inputState.jumping() && grounded) { if (inputState.jumping() && grounded) {
v.y = 0.6f; v.y = 15f;
} }
final float horizontalAcceleration = 5;
// Compute horizontal motion separately. // Compute horizontal motion separately.
if (grounded) { if (grounded) {
if (inputState.forward()) a.z -= 1; if (inputState.forward()) a.z -= 1;
@ -118,9 +120,17 @@ public class WorldUpdater implements Runnable {
Matrix4f moveTransform = new Matrix4f(); Matrix4f moveTransform = new Matrix4f();
moveTransform.rotate(player.getOrientation().x, new Vector3f(0, 1, 0)); moveTransform.rotate(player.getOrientation().x, new Vector3f(0, 1, 0));
moveTransform.transformDirection(a); moveTransform.transformDirection(a);
a.mul(horizontalAcceleration);
hv.add(a); 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) { if (hv.length() > maxSpeed) {
hv.normalize(maxSpeed); hv.normalize(maxSpeed);
} }
@ -132,7 +142,9 @@ public class WorldUpdater implements Runnable {
// Apply velocity to the player's position. // Apply velocity to the player's position.
if (v.lengthSquared() > 0) { if (v.lengthSquared() > 0) {
p.add(v); Vector3f scaledVelocity = new Vector3f(v);
scaledVelocity.mul(secondsPerTick);
p.add(scaledVelocity);
updated = true; updated = true;
} }