Compare commits

...

27 Commits

Author SHA1 Message Date
d3dc397353 Implement Texture Units, bugfix guessChannelCountAndInternalFormat 2025-11-13 06:53:42 +00:00
ad56624b51 Texture: allow png/jpg to be used interchangeably 2025-11-13 00:44:48 +00:00
bb24efb856 Texture implementation 2025-11-01 08:45:36 +00:00
6f731c2885 Remove update website comment from pom.xml 2025-11-01 06:03:12 +00:00
dc50ee48c2 FaderShader class and SplashScene logic 2025-10-25 03:21:09 +01:00
0e66f20423 Simplify SceneManager initial scene logic 2025-10-25 03:20:35 +01:00
2ed1763b83 Add 'DEBUG' LogType and Log::debug function 2025-10-25 03:19:10 +01:00
abd4597f69 Added fader frag shader 2025-10-25 03:18:02 +01:00
d2c33b529a Minor function/variable name changes 2025-10-25 00:26:45 +01:00
9d74dfd613 Refactor Scene swapping logic, refactor Log class 2025-10-25 00:17:30 +01:00
73c4b507e2 Separate mesh logic out of Shader class into Mesh class 2025-10-24 23:10:40 +01:00
1402cbcc47 Window refactors 2025-10-22 23:16:09 +01:00
1aa9546b73 Cleanup SplashScene class 2025-10-22 22:46:20 +01:00
fbb38d1b86 Added default SceneType for SceneManager
This is used as the initial scene that is loaded when the SceneManager
is created.
2025-10-22 22:30:09 +01:00
78efce5cb3 Small refactors and Log::Success function 2025-08-20 22:08:48 +01:00
5900117194 Hacked together rainbow square 2025-08-20 00:02:16 +01:00
1002003168 Added README.md 2025-08-19 23:04:31 +01:00
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
28 changed files with 1016 additions and 71 deletions

1
.gitignore vendored
View File

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

5
README.md Normal file
View File

@@ -0,0 +1,5 @@
# Epic game
The game does NOTHING still
The setup NEVER ends

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

View File

@@ -0,0 +1,10 @@
#version 330 core
in vec4 fColour;
out vec4 colour;
uniform float u_alpha;
void main() {
colour = vec4(fColour.xyz, 1.0 - u_alpha);
}

View File

@@ -0,0 +1,19 @@
#version 330 core
out vec4 FragColour;
in vec4 ourColour;
in vec2 TexCoord;
uniform sampler2D texture1;
uniform sampler2D texture2;
void main()
{
vec4 tex1Color = texture(texture1, TexCoord);
vec4 tex2Color = texture(texture2, TexCoord);
if (tex2Color.a < 0.1)
tex2Color = tex1Color;
FragColour = mix(tex1Color, tex2Color, 0.4f);
}

View File

@@ -0,0 +1,15 @@
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec4 aColour;
layout (location = 2) in vec2 aTexCoord;
out vec4 ourColour;
out vec2 TexCoord;
void main()
{
gl_Position = vec4(aPos, 1.0);
ourColour = aColour;
TexCoord = aTexCoord;
}

BIN
assets/textures/flames.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
assets/textures/plink.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
assets/textures/plink.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

18
pom.xml
View File

@@ -10,7 +10,6 @@
<name>game</name> <name>game</name>
<description>A simple game.</description> <description>A simple game.</description>
<!-- FIXME change it to the project's website -->
<url>http://www.hirw.org</url> <url>http://www.hirw.org</url>
<properties> <properties>
@@ -35,9 +34,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,58 @@
package org.hirw.game;
import static org.lwjgl.opengl.GL20.*;
import lombok.Getter;
import lombok.Setter;
import org.hirw.game.util.Log;
import org.hirw.game.util.Time;
public class FaderShader extends Shader {
private static final String FADER_SHADER_FRAG_PATH = "assets/shaders/fader.frag.glsl";
private static final float DEFAULT_ALPHA_INCREASE_STEP = 1.0f / 3;
private final float INITIAL_ALPHA = 0.0f;
@Getter private final float alphaIncreaseStep;
@Getter @Setter private float alpha;
@Getter @Setter private int alphaUniformLocation;
public FaderShader(float alphaIncreaseStep) {
super(FADER_SHADER_FRAG_PATH, DEFAULT_VERT_PATH);
this.alpha = INITIAL_ALPHA;
this.alphaIncreaseStep = alphaIncreaseStep;
}
public FaderShader() {
this(DEFAULT_ALPHA_INCREASE_STEP);
}
public void init() {
super.init();
findAlphaUniformLocation();
}
public void update() {
super.update();
increaseAlpha();
}
private void findAlphaUniformLocation() {
int location = glGetUniformLocation(getShaderProgramID(), "u_alpha");
if (location == -1) {
Log.warning("FaderShader", "Failed to get 'u_alpha' uniform location, trying to continue...");
} else {
setAlphaUniformLocation(location);
}
}
private void increaseAlpha() {
if (getAlphaUniformLocation() == -1) return;
if (getAlpha() >= 1.0f) return;
setAlpha(getAlpha() + (getAlphaIncreaseStep() * Time.deltaTime()));
glUseProgram(getShaderProgramID());
glUniform1f(getAlphaUniformLocation(), getAlpha());
glUseProgram(0);
}
}

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,118 @@
package org.hirw.game;
import static org.lwjgl.opengl.ARBVertexArrayObject.glBindVertexArray;
import static org.lwjgl.opengl.ARBVertexArrayObject.glGenVertexArrays;
import static org.lwjgl.opengl.GL20.*;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import lombok.Getter;
import lombok.Setter;
import org.lwjgl.BufferUtils;
public class Mesh {
@Getter @Setter private int vaoID, vboID, eboID;
@Getter protected Shader shader;
@Getter float[] vertices;
@Getter int[] elements;
protected final int POSITION_SIZE = 3;
protected final int RGBA_SIZE = 4;
protected final int FLOAT_SIZE_IN_BYTES = Float.SIZE / Byte.SIZE;
protected static final float[] DEFAULT_VERTEX_ARRAY = {
0.5f, -0.5f, 0.0f, /* */ 1.0f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, 0.0f, /* */ 0.0f, 1.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.0f, /* */ 0.0f, 0.0f, 1.0f, 1.0f,
-0.5f, -0.5f, 0.0f, /* */ 1.0f, 1.0f, 0.0f, 1.0f,
};
protected static final int[] DEFAULT_ELEMENT_ARRAY = {
2, 1, 0,
0, 1, 3
};
public Mesh(Shader shader, float[] vertexArray, int[] elementArray) {
this.shader = shader;
this.vaoID = -1;
this.vboID = -1;
this.eboID = -1;
this.vertices = vertexArray;
this.elements = elementArray;
}
public Mesh(Shader shader) {
this(shader, DEFAULT_VERTEX_ARRAY, DEFAULT_ELEMENT_ARRAY);
}
public void init() {
initialiseVertexArrayObject();
}
public void draw() {
getShader().use();
glDraw();
getShader().detach();
}
public void log() {
System.out.println(this.toString());
}
public String toString() {
return String.format(
"VAO: %d, VBO: %d, EBO: %d, ShaderProgram: %d",
getEboID(), getVboID(), getEboID(), getShader().getShaderProgramID());
}
protected void glDraw() {
glBindVertexArray(this.vaoID);
glDrawElements(GL_TRIANGLES, getElements().length, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
}
private void initialiseVertexArrayObject() {
setVaoID(glGenVertexArrays());
glBindVertexArray(getVaoID());
initialiseVertexBufferObject();
initialiseElementBufferObject();
initialiseAttribPointers();
}
protected int initialiseAttribPointers() {
int vertexSizeInBytes = calculateVertexSizeInBytes();
glVertexAttribPointer(0, POSITION_SIZE, GL_FLOAT, false, vertexSizeInBytes, 0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(
1, RGBA_SIZE, GL_FLOAT, false, vertexSizeInBytes, POSITION_SIZE * FLOAT_SIZE_IN_BYTES);
glEnableVertexAttribArray(1);
return vertexSizeInBytes;
}
protected int calculateVertexSizeInBytes() {
return (POSITION_SIZE + RGBA_SIZE) * FLOAT_SIZE_IN_BYTES;
}
private void initialiseVertexBufferObject() {
FloatBuffer vertexBuffer = BufferUtils.createFloatBuffer(getVertices().length);
vertexBuffer.put(getVertices()).flip();
setVboID(glGenBuffers());
glBindBuffer(GL_ARRAY_BUFFER, getVboID());
glBufferData(GL_ARRAY_BUFFER, vertexBuffer, GL_STATIC_DRAW);
}
private void initialiseElementBufferObject() {
IntBuffer elementBuffer = BufferUtils.createIntBuffer(getElements().length);
elementBuffer.put(getElements()).flip();
setEboID(glGenBuffers());
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, getEboID());
glBufferData(GL_ELEMENT_ARRAY_BUFFER, elementBuffer, GL_STATIC_DRAW);
}
}

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,19 @@
package org.hirw.game;
import lombok.Getter;
public abstract class Scene {
@Getter private final SceneType sceneType;
public Scene(SceneType sceneType) {
this.sceneType = sceneType;
}
public abstract void init();
public abstract void update();
public String toString() {
return getSceneType().toString();
}
}

View File

@@ -0,0 +1,56 @@
package org.hirw.game;
import java.util.EnumMap;
import java.util.Map;
import lombok.Getter;
import org.hirw.game.util.Log;
public final class SceneManager {
private static final SceneType DEFAULT_SCENE_TYPE = SceneType.SPLASH;
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;
public static void init() {
setScene(DEFAULT_SCENE_TYPE);
}
public static void setScene(SceneType sceneType) {
Scene newScene = SCENES.get(sceneType);
if (scene == newScene) {
Log.warning("SceneManager", sameSceneWarningString());
return;
} else {
scene = newScene;
}
scene.init();
logSceneChange();
}
public static void update() {
if (scene.getSceneType() == null) return;
getScene().update();
}
private static void logSceneChange() {
SceneType newSceneType = getScene().getSceneType();
String loadedSceneMessage = String.format("Changed to scene '%s'", newSceneType.toString());
Log.success("SceneManager", loadedSceneMessage);
}
private static String sameSceneWarningString() {
return String.format(
"Tried to switch to scene '%s' but that scene is already loaded",
scene.getSceneType().toString());
}
}

View File

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

View File

@@ -0,0 +1,128 @@
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));
protected static final String DEFAULT_FRAG_PATH = "assets/shaders/default.frag.glsl";
protected 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(String path) {
this(path + ".frag.glsl", path + ".vert.glsl");
}
public Shader() {
this(DEFAULT_FRAG_PATH, DEFAULT_VERT_PATH);
}
public void init() {
compileShader(ShaderType.FRAG);
compileShader(ShaderType.VERT);
createProgram();
}
public void use() {
glUseProgram(getShaderProgramID());
}
public void detach() {
glUseProgram(0);
}
public void update() {}
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",
String.format(
"Failed to compile %s shader: %s",
shaderType.toString(), glGetShaderInfoLog(shaderID, len)));
}
}
private void createProgram() {
int id = glCreateProgram();
glAttachShader(id, getVertexID());
glAttachShader(id, getFragmentID());
glLinkProgram(id);
int success = glGetProgrami(id, GL_LINK_STATUS);
if (success == GL_FALSE) {
int len = glGetProgrami(id, GL_INFO_LOG_LENGTH);
Log.error(
"Shader",
String.format("Failed to create Shader Program: %s", glGetShaderInfoLog(id, len)));
} else {
Log.success("Shader", "Shader Program created.");
}
setShaderProgramID(id);
}
private String readFromFile(String stringFilePath) {
String source = "";
try {
Path filePath = Paths.get(stringFilePath);
source = Files.readString(filePath);
} catch (NoSuchFileException | InvalidPathException e) {
Log.error("Shader", "Couldn't open file (probably a bad path): " + stringFilePath);
} catch (IOException e) {
Log.error("Shader", "An IO Exception occured while reading from file: " + stringFilePath);
}
return source;
}
}

View File

@@ -0,0 +1,72 @@
package org.hirw.game;
import lombok.Getter;
import lombok.Setter;
public class SplashScene extends Scene {
@Getter public final SceneType SCENE_TYPE = SceneType.SPLASH;
@Getter @Setter private Mesh screenCover;
@Getter @Setter private FaderShader screenCoverFaderShader;
@Getter @Setter private Mesh logo;
public SplashScene() {
super(SceneType.SPLASH);
}
public void init() {
createScreenCover();
createLogo();
}
public void update() {
getLogo().draw();
getScreenCoverFaderShader().update();
getScreenCover().draw();
}
private void createScreenCover() {
FaderShader faderShader = new FaderShader();
faderShader.init();
Mesh screenCover = new Mesh(faderShader, screenCoverRectVertices, screenCoverRectElements);
screenCover.init();
setScreenCoverFaderShader(faderShader);
setScreenCover(screenCover);
}
private void createLogo() {
Shader texturedShader = new Shader("assets/shaders/texture");
texturedShader.init();
Texture logoTexture = new Texture(225, 225, "assets/textures/plink.png");
Texture flameTexture = new Texture(2500, 2500, "assets/textures/flames.png");
logoTexture.init();
flameTexture.init();
Mesh logoMesh =
new TexturedMesh(
texturedShader,
new Texture[] {logoTexture, flameTexture},
logoRectVertices,
screenCoverRectElements);
logoMesh.init();
setLogo(logoMesh);
}
private static final float[] screenCoverRectVertices = {
1.0f, 1.0f, 0.0f, /* */ 0.0f, 0.0f, 0.0f, 0.0f, // 0 Top Right
1.0f, -1.0f, 0.0f, /* */ 0.0f, 0.0f, 0.0f, 0.0f, // 1 Bottom Right
-1.0f, -1.0f, 0.0f, /* */ 0.0f, 0.0f, 0.0f, 0.0f, // 2 Bottom Left
-1.0f, 1.0f, 0.0f, /* */ 0.0f, 0.0f, 0.0f, 0.0f, // 3 Top Left
};
private static final int[] screenCoverRectElements = {
0, 1, 3,
1, 2, 3
};
private static final float[] logoRectVertices = {
0.1f, 0.1f, 0.0f, /* */ 0.0f, 0.0f, 0.0f, 0.0f, /* */ 1.0f, 1.0f, // 0 Top Right
0.1f, -0.1f, 0.0f, /* */ 0.0f, 0.0f, 0.0f, 0.0f, /* */ 1.0f, 0.0f, // 1 Bottom Right
-0.1f, -0.1f, 0.0f, /* */ 0.0f, 0.0f, 0.0f, 0.0f, /* */ 0.0f, 0.0f, // 2 Bottom Left
-0.1f, 0.1f, 0.0f, /* */ 0.0f, 0.0f, 0.0f, 0.0f, /* */ 0.0f, 1.0f, // 3 Top Left
};
}

View File

@@ -0,0 +1,105 @@
package org.hirw.game;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL12.*;
import static org.lwjgl.stb.STBImage.stbi_image_free;
import static org.lwjgl.stb.STBImage.stbi_load;
import static org.lwjgl.stb.STBImage.stbi_set_flip_vertically_on_load;
import java.nio.ByteBuffer;
import lombok.Getter;
import lombok.Setter;
public class Texture {
@Getter private int width;
@Getter private int height;
@Getter private String texturePath;
@Getter @Setter private int textureID;
@Getter @Setter private int channelCount;
@Getter @Setter private int internalFormat;
public Texture(int width, int height, String texturePath) {
this.width = width;
this.height = height;
this.texturePath = texturePath;
guessChannelCountAndInternalFormat();
}
private void guessChannelCountAndInternalFormat() {
int fileExtensionIndex = getTexturePath().lastIndexOf(".");
if (fileExtensionIndex == -1) {
// No file extension, so just assume 4 channels (.png -> RGBA)
setChannelCount(4);
setInternalFormat(GL_RGBA);
return;
}
String fileExtension = getTexturePath().substring(fileExtensionIndex + 1);
switch (fileExtension) {
case "png":
setChannelCount(4);
setInternalFormat(GL_RGBA);
break;
case "jpg":
setChannelCount(3);
setInternalFormat(GL_RGB);
break;
default:
// just pray at this point
setChannelCount(3);
setInternalFormat(GL_RGB);
}
}
public void init() {
ByteBuffer imageBytes = loadImageBytes();
createTexture(imageBytes);
stbi_image_free(imageBytes);
}
private ByteBuffer loadImageBytes() {
stbi_set_flip_vertically_on_load(true);
return stbi_load(
getTexturePath(),
new int[getWidth()],
new int[getHeight()],
new int[getChannelCount()],
getChannelCount());
}
private void createTexture(ByteBuffer imageBytes) {
setTextureID(glGenTextures());
glBindTexture(GL_TEXTURE_2D, getTextureID());
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
setupFilterParameters();
setupWrapParameters();
glTexImage2D(
GL_TEXTURE_2D,
0,
getInternalFormat(),
getWidth(),
getHeight(),
0,
getInternalFormat(),
GL_UNSIGNED_BYTE,
imageBytes);
// glGenerateMipmap(GL_TEXTURE_2D);
}
private void setupFilterParameters() {
// https://github.com/mattdesl/lwjgl-basics/wiki/textures#texture-parameters
// Set the minification and Magnifiation filters:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
}
private void setupWrapParameters() {
// https://github.com/mattdesl/lwjgl-basics/wiki/textures#texture-parameters
// Each Vertex has many attributes, including Position (x, y) and Texture Coordinates (s, t).
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
}
}

View File

@@ -0,0 +1,47 @@
package org.hirw.game;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL20.*;
import lombok.Getter;
public class TexturedMesh extends Mesh {
@Getter private Texture[] textures;
private final int TEXTURE_COORDS_SIZE = 2;
public TexturedMesh(Shader shader, Texture[] textures, float[] vertexArray, int[] elementArray) {
super(shader, vertexArray, elementArray);
this.textures = textures;
}
public TexturedMesh(Shader shader, Texture[] textures) {
this(shader, textures, DEFAULT_VERTEX_ARRAY, DEFAULT_ELEMENT_ARRAY);
}
protected int initialiseAttribPointers() {
int vertexSizeInBytes = super.initialiseAttribPointers();
int offset = (POSITION_SIZE + RGBA_SIZE) * FLOAT_SIZE_IN_BYTES;
glVertexAttribPointer(2, TEXTURE_COORDS_SIZE, GL_FLOAT, false, vertexSizeInBytes, offset);
glEnableVertexAttribArray(2);
return vertexSizeInBytes;
}
protected int calculateVertexSizeInBytes() {
return (POSITION_SIZE + RGBA_SIZE + TEXTURE_COORDS_SIZE) * FLOAT_SIZE_IN_BYTES;
}
protected void glDraw() {
for (int i = 0; i < textures.length; i++) {
int textureUnitIndexStart = GL_TEXTURE0;
int textureUnitIndex = textureUnitIndexStart + i;
glActiveTexture(textureUnitIndex);
glBindTexture(GL_TEXTURE_2D, getTextures()[i].getTextureID());
String uniformName = "texture" + Integer.toString(i + 1);
glUniform1i(glGetUniformLocation(getShader().getShaderProgramID(), uniformName), i);
}
super.glDraw();
}
}

View File

@@ -3,22 +3,25 @@ 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 lombok.Setter;
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; @Getter private String title;
private long glfwWindow; @Getter private long glfwWindow;
private static Window window = null; private static Window window = null;
@Getter @Setter private Shader shader;
@Getter @Setter private Mesh mesh;
private Window() { private Window() {
this.width = 1280; this.width = 1280;
this.height = 720; this.height = 720;
@@ -26,25 +29,31 @@ public class Window {
} }
public static Window get() { public static Window get() {
if (Window.window == null) { if (window == null) {
Window.window = new Window(); window = new Window();
} }
return window;
return Window.window;
} }
public void blastOff() { public void blastOff() {
logVersion(); init();
setup();
loop(); loop();
cleanUp(); cleanUp();
} }
private void init() {
logVersion();
createWindow();
createShader();
createMesh();
SceneManager.init();
}
private void logVersion() { private void logVersion() {
System.out.println("LWJGL Version: " + Version.getVersion()); System.out.println("LWJGL Version: " + Version.getVersion());
} }
private void setup() { private void createWindow() {
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");
@@ -52,36 +61,46 @@ public class Window {
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE); glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);
glfwWindow = glfwCreateWindow(this.width, this.height, "Hello World!", NULL, NULL); glfwWindow = glfwCreateWindow(this.width, this.height, getTitle(), NULL, NULL);
if (glfwWindow == NULL) throw new RuntimeException("Failed to create the GLFW window"); if (glfwWindow == NULL) throw new RuntimeException("Failed to create the GLFW window");
// glfwSetKeyCallback( glfwSetCursorPosCallback(glfwWindow, Mouse::cursorPositionCallback);
// glfwWindow, glfwSetMouseButtonCallback(glfwWindow, Mouse::mouseButtonCallback);
// (glfwWindow, key, scancode, action, mods) -> { glfwSetKeyCallback(glfwWindow, Keyboard::keyCallback);
// if (key == GLFW_KEY_ESCAPE && action == GLFW_RELEASE)
// glfwSetWindowShouldClose(glfwWindow, true);
// });
//
// try (MemoryStack stack = stackPush()) {
// IntBuffer pWidth = stack.mallocInt(1);
// IntBuffer pHeight = stack.mallocInt(1);
// glfwGetWindowSize(glfwWindow, pWidth, pHeight);
// GLFWVidMode vidmode = glfwGetVideoMode(glfwGetPrimaryMonitor());
// }
glfwSetWindowTitle(glfwWindow, this.title); glfwSetWindowTitle(glfwWindow, this.title);
glfwMakeContextCurrent(glfwWindow); glfwMakeContextCurrent(glfwWindow);
glfwSwapInterval(1); glfwSwapInterval(1);
glfwShowWindow(glfwWindow); glfwShowWindow(glfwWindow);
GL.createCapabilities(); GL.createCapabilities();
glEnable(GL_BLEND);
glEnable(GL_TEXTURE_2D);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glClearColor(1.0f, 1.0f, 1.0f, 0.0f);
}
private void createShader() {
Shader shader = new Shader();
setShader(shader);
shader.init();
}
private void createMesh() {
Mesh mesh = new Mesh(getShader());
setMesh(mesh);
mesh.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();
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
getMesh().draw();
SceneManager.update();
glfwSwapBuffers(glfwWindow); glfwSwapBuffers(glfwWindow);
glfwPollEvents(); glfwPollEvents();
} }

View File

@@ -0,0 +1,68 @@
package org.hirw.game.util;
import java.util.EnumMap;
import java.util.Map;
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 BLUE_BG = "\u001B[44m";
final String PURPLE = "\u001B[35m";
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";
}
private static final EnumMap<LogType, String> COLOUR_FROM_LOG_TYPE =
new EnumMap<>(
Map.of(
LogType.SUCCESS, Colours.GREEN,
LogType.ERROR, Colours.RED,
LogType.WARNING, Colours.PURPLE,
LogType.DEBUG, Colours.WHITE));
public static void error(String errorStage, String errorDescription) {
System.out.println(formatString(LogType.ERROR, errorStage, errorDescription));
}
public static void success(String successStage, String successDescription) {
System.out.println(formatString(LogType.SUCCESS, successStage, successDescription));
}
public static void warning(String warningStage, String warningDescription) {
System.out.println(formatString(LogType.WARNING, warningStage, warningDescription));
}
public static void debug(String debugMessage) {
System.out.println(formatString(LogType.DEBUG, "DEBUG", debugMessage));
}
private static String formatString(LogType logType, String stage, String description) {
String formattedType = String.format("[%s]", colouriseString(logType, logType.toString()));
String formattedStage = colouriseString(Colours.YELLOW, String.format("<%s>", stage));
return String.format("%s %s %s", formattedType, formattedStage, description);
}
private static String colouriseString(LogType logType, String string) {
return COLOUR_FROM_LOG_TYPE.get(logType) + string + Colours.ANSI_RESET;
}
private static String colouriseString(String colour, String string) {
return colour + string + Colours.ANSI_RESET;
}
private static String colouriseString(String colour, String otherColour, String string) {
return colour + otherColour + string + Colours.ANSI_RESET;
}
}

View File

@@ -0,0 +1,8 @@
package org.hirw.game.util;
public enum LogType {
SUCCESS,
WARNING,
ERROR,
DEBUG,
}

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