Compare commits

...

10 Commits

Author SHA1 Message Date
c28ec45f8c Added Shader class, default shaders 2025-08-19 19:56:37 +01:00
f13d4e3902 Added Log class 2025-08-19 19:54:33 +01:00
eb3a5e7dd1 Rough Scene/SceneManager implementation 2025-08-18 20:48:59 +01:00
9cf088cc6c Added Time class + tests 2025-08-17 21:50:51 +01:00
42f162a03a Remove AppTest.java 2025-08-17 21:49:26 +01:00
edff4b27db Add Mockito, upgrade JUnit 2025-08-17 21:49:00 +01:00
2a66ed1062 Add .factorypath to gitignore 2025-08-17 15:26:17 +01:00
1b7b9e4ffd Added Mouse/Keyboard classes 2025-08-17 15:11:42 +01:00
31d8cac12a Add Lombok 2025-08-15 10:09:50 +01:00
38c5021090 Window: Cleanup imports 2025-08-14 23:06:38 +01:00
16 changed files with 452 additions and 49 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ target/
.classpath .classpath
.project .project
.factorypath

View File

@@ -0,0 +1,8 @@
#version 330 core
in vec4 fColour;
out vec4 colour;
void main() {
colour = fColour;
}

View File

@@ -0,0 +1,11 @@
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec4 aColour;
out vec4 fColour;
void main() {
fColour = aColour;
gl_Position = vec4(aPos, 1.0);
}

17
pom.xml
View File

@@ -35,9 +35,20 @@
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>junit</groupId> <groupId>org.mockito</groupId>
<artifactId>junit</artifactId> <artifactId>mockito-core</artifactId>
<version>3.8.1</version> <version>5.19.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.38</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.13.4</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.lwjgl</groupId> <groupId>org.lwjgl</groupId>

View File

@@ -0,0 +1,37 @@
package org.hirw.game;
import static org.lwjgl.glfw.GLFW.*;
import java.util.HashMap;
public class Keyboard {
private static Keyboard instance;
private HashMap<Integer, Boolean> buttons;
private Keyboard() {
this.buttons = new HashMap<Integer, Boolean>();
}
public static Keyboard getInstance() {
if (instance == null) {
synchronized (Keyboard.class) {
if (instance == null) {
instance = new Keyboard();
}
}
}
return instance;
}
public static void keyCallback(long window, int keyCode, int _scan, int action, int _modifiers) {
if (action == GLFW_PRESS) {
getInstance().buttons.put(keyCode, true);
} else if (action == GLFW_RELEASE) {
getInstance().buttons.put(keyCode, false);
}
}
public static boolean isPressed(int keyCode) {
return getInstance().buttons.getOrDefault(keyCode, false);
}
}

View File

@@ -0,0 +1,67 @@
package org.hirw.game;
import static org.lwjgl.glfw.GLFW.*;
import java.util.HashMap;
public class Mouse {
private double x, y, oldX, oldY;
private HashMap<Integer, Boolean> buttons;
private static Mouse instance;
public interface Buttons {
public final int LEFT = GLFW_MOUSE_BUTTON_LEFT;
public final int RIGHT = GLFW_MOUSE_BUTTON_RIGHT;
}
private Mouse() {
this.buttons = new HashMap<Integer, Boolean>();
}
public static Mouse getInstance() {
if (instance == null) {
synchronized (Mouse.class) { // Double checked locking
if (instance == null) {
instance = new Mouse();
}
}
}
return instance;
}
public static void cursorPositionCallback(long window, double newX, double newY) {
getInstance().oldX = getInstance().x;
getInstance().oldY = getInstance().y;
getInstance().x = newX;
getInstance().y = newY;
}
public static void mouseButtonCallback(long window, int keyCode, int action, int _modifiers) {
if (action == GLFW_PRESS) {
getInstance().buttons.put(keyCode, true);
} else if (action == GLFW_RELEASE) {
getInstance().buttons.put(keyCode, false);
}
}
public static double getX() {
return getInstance().x;
}
public static double getY() {
return getInstance().y;
}
public static double getDeltaX() {
return getInstance().oldX - getInstance().x;
}
public static double getDeltaY() {
return getInstance().oldY - getInstance().y;
}
public static boolean isPressed(int keyCode) {
return getInstance().buttons.getOrDefault(keyCode, false);
}
}

View File

@@ -0,0 +1,8 @@
package org.hirw.game;
public abstract class Scene {
public Scene() {}
abstract void update();
}

View File

@@ -0,0 +1,20 @@
package org.hirw.game;
import java.util.EnumMap;
import java.util.Map;
import lombok.Getter;
final class SceneManager {
private static final EnumMap<SceneType, Scene> SCENES =
new EnumMap<>(
Map.of(
SceneType.SPLASH, new SplashScene(),
SceneType.MENU, new SplashScene(),
SceneType.GAME, new SplashScene()));
@Getter private static Scene scene = SCENES.get(SceneType.SPLASH);
public static void setScene(SceneType sType) {
scene = SCENES.get(sType);
}
}

View File

@@ -0,0 +1,7 @@
package org.hirw.game;
public enum SceneType {
SPLASH,
MENU,
GAME
}

View File

@@ -0,0 +1,115 @@
package org.hirw.game;
import static org.lwjgl.opengl.GL20.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.EnumMap;
import java.util.Map;
import lombok.Getter;
import lombok.Setter;
import org.hirw.game.util.Log;
public class Shader {
private enum ShaderType {
FRAG,
VERT
}
private static final EnumMap<ShaderType, Integer> SHADERS =
new EnumMap<>(
Map.of(
ShaderType.FRAG, GL_FRAGMENT_SHADER,
ShaderType.VERT, GL_VERTEX_SHADER));
private static final String DEFAULT_FRAG_PATH = "assets/shaders/default.frag.glsl";
private static final String DEFAULT_VERT_PATH = "assets/shaders/default.vert.glsl";
@Getter private String vertexSource;
@Getter private String fragmentSource;
@Getter @Setter private int vertexID;
@Getter @Setter private int fragmentID;
@Getter @Setter private int shaderProgramID;
public Shader(String fragPath, String vertPath) {
this.fragmentSource = readFromFile(fragPath);
this.vertexSource = readFromFile(vertPath);
}
public Shader() {
this(DEFAULT_FRAG_PATH, DEFAULT_VERT_PATH);
}
public void init() {
compileShader(ShaderType.FRAG);
compileShader(ShaderType.VERT);
createProgram();
}
private void compileShader(ShaderType shaderType) {
int shaderID = glCreateShader(SHADERS.get(shaderType));
switch (shaderType) {
case ShaderType.FRAG -> {
setFragmentID(shaderID);
glShaderSource(shaderID, getFragmentSource());
}
case ShaderType.VERT -> {
setVertexID(shaderID);
glShaderSource(shaderID, getVertexSource());
}
}
glCompileShader(shaderID);
if (glGetShaderi(shaderID, GL_COMPILE_STATUS) == GL_FALSE) {
int len = glGetShaderi(shaderID, GL_INFO_LOG_LENGTH);
Log.error(
"Shader initialisation",
String.format(
"Failed to compile %s shader: %s",
shaderType.toString(), glGetShaderInfoLog(shaderID, len)));
}
}
private void createProgram() {
setShaderProgramID(glCreateProgram());
glAttachShader(shaderProgramID, getVertexID());
glAttachShader(shaderProgramID, getFragmentID());
glLinkProgram(shaderProgramID);
int success = glGetProgrami(getShaderProgramID(), GL_LINK_STATUS);
if (success == GL_FALSE) {
int len = glGetProgrami(getShaderProgramID(), GL_INFO_LOG_LENGTH);
Log.error(
"Shader initialisation",
String.format(
"Failed to create Shader Program: %s",
glGetShaderInfoLog(getShaderProgramID(), len)));
}
}
private String readFromFile(String stringFilePath) {
String source = "";
try {
Path filePath = Paths.get(stringFilePath);
source = Files.readString(filePath);
} catch (NoSuchFileException | InvalidPathException e) {
Log.error(
"Shader initialisation", "Couldn't open file (probably a bad path): " + stringFilePath);
} catch (IOException e) {
Log.error(
"Shader initialisation",
"An IO Exception occured while reading from file: " + stringFilePath);
}
return source;
}
}

View File

@@ -0,0 +1,19 @@
package org.hirw.game;
import static org.lwjgl.opengl.GL11.*;
import java.util.Objects;
import org.hirw.game.util.Time;
public class SplashScene extends Scene {
private float ramp = 0.0f;
public void update() {
if (Objects.isNull(Window.get().getGlfwWindow())) {
return;
}
glClearColor(this.ramp, this.ramp, this.ramp, 0.0f);
this.ramp += 0.5f * Time.deltaTime();
}
}

View File

@@ -3,19 +3,18 @@ package org.hirw.game;
import static org.lwjgl.glfw.Callbacks.*; import static org.lwjgl.glfw.Callbacks.*;
import static org.lwjgl.glfw.GLFW.*; import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.opengl.GL11.*; import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.system.MemoryStack.*;
import static org.lwjgl.system.MemoryUtil.*; import static org.lwjgl.system.MemoryUtil.*;
import java.nio.*; import lombok.Getter;
import org.hirw.game.util.Time;
import org.lwjgl.Version; import org.lwjgl.Version;
import org.lwjgl.glfw.*; import org.lwjgl.glfw.*;
import org.lwjgl.opengl.*; import org.lwjgl.opengl.*;
import org.lwjgl.system.*;
public class Window { public class Window {
private int width, height; private int width, height;
private final String title; private final String title;
private long glfwWindow; @Getter private long glfwWindow;
private static Window window = null; private static Window window = null;
@@ -34,7 +33,6 @@ public class Window {
} }
public void blastOff() { public void blastOff() {
logVersion();
setup(); setup();
loop(); loop();
cleanUp(); cleanUp();
@@ -45,6 +43,8 @@ public class Window {
} }
private void setup() { private void setup() {
logVersion();
GLFWErrorCallback.createPrint(System.err).set(); GLFWErrorCallback.createPrint(System.err).set();
if (!glfwInit()) throw new IllegalStateException("Unable to initialize GLFW"); if (!glfwInit()) throw new IllegalStateException("Unable to initialize GLFW");
@@ -69,18 +69,25 @@ public class Window {
// GLFWVidMode vidmode = glfwGetVideoMode(glfwGetPrimaryMonitor()); // GLFWVidMode vidmode = glfwGetVideoMode(glfwGetPrimaryMonitor());
// } // }
glfwSetCursorPosCallback(glfwWindow, Mouse::cursorPositionCallback);
glfwSetMouseButtonCallback(glfwWindow, Mouse::mouseButtonCallback);
glfwSetKeyCallback(glfwWindow, Keyboard::keyCallback);
glfwSetWindowTitle(glfwWindow, this.title); glfwSetWindowTitle(glfwWindow, this.title);
glfwMakeContextCurrent(glfwWindow); glfwMakeContextCurrent(glfwWindow);
glfwSwapInterval(1); glfwSwapInterval(1);
glfwShowWindow(glfwWindow); glfwShowWindow(glfwWindow);
GL.createCapabilities(); GL.createCapabilities();
glClearColor(0.0f, 0.0f, 2.0f, 0.0f);
Shader someShader = new Shader();
someShader.init();
} }
private void loop() { private void loop() {
// GL.createCapabilities(); // Does this maybe go in here?
glClearColor(1.0f, 0.0f, 0.0f, 0.0f);
while (!glfwWindowShouldClose(glfwWindow)) { while (!glfwWindowShouldClose(glfwWindow)) {
Time.update();
SceneManager.getScene().update();
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glfwSwapBuffers(glfwWindow); glfwSwapBuffers(glfwWindow);
glfwPollEvents(); glfwPollEvents();

View File

@@ -0,0 +1,35 @@
package org.hirw.game.util;
public final class Log {
private interface Colours {
final String BLACK = "\u001B[30m";
final String BLACK_BG = "\u001B[40m";
final String RED = "\u001B[31m";
final String RED_BG = "\u001B[41m";
final String GREEN = "\u001B[32m";
final String GREEN_BG = "\u001B[42m";
final String YELLOW = "\u001B[33m";
final String YELLOW_BG = "\u001B[43m";
final String BLUE = "\u001B[34m";
final String PURPLE_BG = "\u001B[45m";
final String CYAN = "\u001B[36m";
final String CYAN_BG = "\u001B[46m";
final String WHITE = "\u001B[37m";
final String WHITE_BG = "\u001B[47m";
final String ANSI_RESET = "\u001B[0m";
}
public static void error(String errorStage, String errorDescription) {
String fancyError = String.format("[%s] ", colourisedString(Colours.RED, "ERROR"));
String fancyErrorStage = colourisedString(Colours.YELLOW, String.format("<%s> ", errorStage));
System.err.println(fancyError + fancyErrorStage + errorDescription);
}
private static String colourisedString(String colour, String string) {
return colour + string + Colours.ANSI_RESET;
}
private static String colourisedString(String colour, String otherColour, String string) {
return colour + otherColour + string + Colours.ANSI_RESET;
}
}

View File

@@ -0,0 +1,31 @@
package org.hirw.game.util;
public final class Time {
private static long lastTime = NanoTimer.system().nanoTime();
private static long currentTime = NanoTimer.system().nanoTime();
private static NanoTimer nanoTimer = NanoTimer.system();
public interface NanoTimer {
long nanoTime();
static NanoTimer system() {
return System::nanoTime;
}
}
static void setNanoTimer(NanoTimer timer) {
nanoTimer = timer;
lastTime = nanoTimer.nanoTime();
currentTime = nanoTimer.nanoTime();
}
public static void update() {
lastTime = currentTime;
currentTime = nanoTimer.nanoTime();
}
public static float deltaTime() {
final float ONE_SECOND = 1_000_000_000f;
return (currentTime - lastTime) / ONE_SECOND;
}
}

View File

@@ -1,38 +0,0 @@
package org.hirw.game;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
/**
* Unit test for simple App.
*/
public class AppTest
extends TestCase
{
/**
* Create the test case
*
* @param testName name of the test case
*/
public AppTest( String testName )
{
super( testName );
}
/**
* @return the suite of tests being tested
*/
public static Test suite()
{
return new TestSuite( AppTest.class );
}
/**
* Rigourous Test :-)
*/
public void testApp()
{
assertTrue( true );
}
}

View File

@@ -0,0 +1,64 @@
package org.hirw.game.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
class TimeTest {
final float ONE_SECOND = 1_000_000_000f;
final long ONE_MILLISECOND = 1_000_000L;
final float MOE = 0.00000001f; // Margin of error
private Time.NanoTimer nanoTimer;
@BeforeEach
void setUp() {
nanoTimer = Mockito.mock(Time.NanoTimer.class);
when(nanoTimer.nanoTime()).thenReturn(0L);
Time.setNanoTimer(nanoTimer);
}
@Test
void testDeltaTimeInitialState() {
float deltaTime = Time.deltaTime();
assertEquals(deltaTime, 0f, MOE, "should be 0");
}
@Test
void testDeltaTimeAfterUpdate() {
when(nanoTimer.nanoTime()).thenReturn(ONE_MILLISECOND);
Time.update();
float deltaTime = Time.deltaTime();
assertEquals(deltaTime, ONE_MILLISECOND / ONE_SECOND, MOE, "should reflect time difference");
}
@Test
void testMultipleUpdates() {
when(nanoTimer.nanoTime()).thenReturn(ONE_MILLISECOND).thenReturn(ONE_MILLISECOND * 4);
Time.update();
float delta1 = Time.deltaTime();
Time.update();
float delta2 = Time.deltaTime();
assertEquals(delta1, ONE_MILLISECOND / ONE_SECOND, MOE, "should be 1/1000th second");
assertEquals(delta2, (ONE_MILLISECOND * 3) / ONE_SECOND, MOE, "should be 3/1000th second");
}
@Test
void testConsistentStateAcrossCalls() {
when(nanoTimer.nanoTime()).thenReturn(ONE_MILLISECOND);
Time.update();
float delta1 = Time.deltaTime();
float delta2 = Time.deltaTime();
assertEquals(delta1, delta2, MOE, "should be equal delta time");
}
}