Refactored client game rendering logic.
This commit is contained in:
parent
aed69f6dfb
commit
9e8574ce21
|
@ -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(
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package nl.andrewl.aos2_client;
|
||||
|
||||
import nl.andrewl.aos_core.model.World;
|
||||
|
||||
public class ClientWorld extends World {
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Chunk> meshGenerationQueue = new ConcurrentLinkedQueue<>();
|
||||
|
||||
private final Matrix4f projectionTransform;
|
||||
private ShaderProgram shaderProgram;
|
||||
private int projectionTransformUniform;
|
||||
private int viewTransformUniform;
|
||||
private int chunkPositionUniform;
|
||||
|
||||
private final List<ChunkMesh> 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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package nl.andrewl.aos2_client.render;
|
||||
|
||||
public record WindowInfo(
|
||||
long windowHandle,
|
||||
int width,
|
||||
int height
|
||||
) {}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@ import java.util.Map;
|
|||
* that players can interact in.
|
||||
*/
|
||||
public class World {
|
||||
private final Map<Vector3ic, Chunk> chunkMap = new HashMap<>();
|
||||
protected final Map<Vector3ic, Chunk> chunkMap = new HashMap<>();
|
||||
|
||||
public void addChunk(Chunk chunk) {
|
||||
chunkMap.put(chunk.getPosition(), chunk);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue