diff --git a/src/main/java/net/whg/we/main/CullGameObjectsAction.java b/src/main/java/net/whg/we/main/CullGameObjectsPipeline.java similarity index 84% rename from src/main/java/net/whg/we/main/CullGameObjectsAction.java rename to src/main/java/net/whg/we/main/CullGameObjectsPipeline.java index 670b412..a423234 100644 --- a/src/main/java/net/whg/we/main/CullGameObjectsAction.java +++ b/src/main/java/net/whg/we/main/CullGameObjectsPipeline.java @@ -3,7 +3,7 @@ import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; -public class CullGameObjectsAction implements IPipelineAction +public class CullGameObjectsPipeline implements IPipelineAction { private final List gameObjects = new CopyOnWriteArrayList<>(); @@ -30,6 +30,6 @@ public void disableGameObject(GameObject gameObject) @Override public int getPriority() { - return 40000; + return PipelineConstants.DISPOSE_GAMEOBJECTS; } } diff --git a/src/main/java/net/whg/we/main/GameObject.java b/src/main/java/net/whg/we/main/GameObject.java index 672eab1..44546f2 100644 --- a/src/main/java/net/whg/we/main/GameObject.java +++ b/src/main/java/net/whg/we/main/GameObject.java @@ -4,7 +4,6 @@ import java.util.List; import java.util.UUID; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.stream.Collectors; import net.whg.we.util.IDisposable; /** @@ -129,11 +128,12 @@ public void removeBehavior(AbstractBehavior behavior) * @return The behavior with the given superclass, or null if no matching * behavior is found. */ - public AbstractBehavior getBehavior(Class behaviorType) + @SuppressWarnings("unchecked") + public T getBehavior(Class behaviorType) { for (AbstractBehavior behavior : behaviors) if (behaviorType.isAssignableFrom(behavior.getClass())) - return behavior; + return (T) behavior; return null; } @@ -156,11 +156,16 @@ public List getBehaviors() * - The type of behaviors to get. * @return A list of behaviors with the given superclass. */ - public List getBehaviors(Class behaviorType) + @SuppressWarnings("unchecked") + public List getBehaviors(Class behaviorType) { - return behaviors.stream() - .filter(behavior -> behaviorType.isAssignableFrom(behavior.getClass())) - .collect(Collectors.toList()); + List list = new ArrayList<>(); + + for (AbstractBehavior behavior : behaviors) + if (behaviorType.isAssignableFrom(behavior.getClass())) + list.add((T) behavior); + + return list; } /** diff --git a/src/main/java/net/whg/we/main/IPipelineAction.java b/src/main/java/net/whg/we/main/IPipelineAction.java index 92f9353..bd7a924 100644 --- a/src/main/java/net/whg/we/main/IPipelineAction.java +++ b/src/main/java/net/whg/we/main/IPipelineAction.java @@ -11,18 +11,7 @@ *

* Pipeline actions should also override the default priority level for loop * actions to ensure pipeline actions occur in the desired order. Default - * priorities are: - *

    - *
  • Calculate Time Stamps at -1000000
  • - *
  • Physics Updates at -10000
  • - *
  • Updates at 0
  • - *
  • Animation Updates at 10000
  • - *
  • Late Updates at 20000
  • - *
  • Rendering Solids at 30000
  • - *
  • Rendering Transparents at 32500
  • - *
  • Dispose GameObjects at 40000
  • - *
  • End Frame at 1000000
  • - *
+ * priorities are defined in the {@link PipelineConstants}. */ public interface IPipelineAction extends ILoopAction { diff --git a/src/main/java/net/whg/we/main/IUpdateable.java b/src/main/java/net/whg/we/main/IUpdateable.java index caa830d..121516f 100644 --- a/src/main/java/net/whg/we/main/IUpdateable.java +++ b/src/main/java/net/whg/we/main/IUpdateable.java @@ -9,6 +9,10 @@ public interface IUpdateable { /** * Called once each frame to update this object. + * + * @param timer + * - The timer associated with the update pipeline. Used to retrieve delta + * times. */ - void update(); + void update(Timer timer); } diff --git a/src/main/java/net/whg/we/main/Input.java b/src/main/java/net/whg/we/main/Input.java deleted file mode 100644 index 8757e56..0000000 --- a/src/main/java/net/whg/we/main/Input.java +++ /dev/null @@ -1,248 +0,0 @@ -package net.whg.we.main; - -import java.util.Arrays; - -/** - * This input class is a reference for the current key and mouse states for the - * focused window. This can be used to check for mouse inputs and key presses as - * they occur. - */ -public final class Input -{ - /** - * The largest key code input provided by GLFW. Used for making sure enough - * memory is allocated to store all key states. - */ - private static final int MAX_INPUT_KEY_CODE = 348; - - /** - * The largest mouse button input provided by GLFW. Used for making sure enough - * memory is allocated to store all mouse button states. - */ - private static final int MAX_INPUT_MOUSE_BUTTON = 7; - - private static boolean[] keyStates = new boolean[MAX_INPUT_KEY_CODE + 1]; - private static boolean[] lastKeyStates = new boolean[MAX_INPUT_KEY_CODE + 1]; - private static boolean[] mouseButtons = new boolean[MAX_INPUT_MOUSE_BUTTON + 1]; - private static boolean[] lastMouseButtons = new boolean[MAX_INPUT_MOUSE_BUTTON + 1]; - private static float mouseX; - private static float mouseY; - private static float lastMouseX; - private static float lastMouseY; - private static float scrollWheelDelta; - - /** - * Clear all data stored in this class, resetting it to default settings. Any - * keys or buttons currently held are assumed unpressed. Mouse position is - * assumed to be at 0, 0. - */ - public static void clear() - { - Arrays.fill(keyStates, false); - Arrays.fill(lastKeyStates, false); - Arrays.fill(mouseButtons, false); - Arrays.fill(lastMouseButtons, false); - - mouseX = 0f; - mouseY = 0f; - lastMouseX = 0f; - lastMouseY = 0f; - scrollWheelDelta = 0f; - } - - /** - * Assigns a new state to a given key. - * - * @param key - * - The key in question. - * @param pressed - * - True if the key was pressed. False if the key was released. - */ - static void setKeyState(final int key, final boolean pressed) - { - keyStates[key] = pressed; - } - - /** - * Assigns a new set of coords to the mouse position. - * - * @param mouseX - * - The current mouse x. - * @param mouseY - * - The current mouse y. - */ - static void setMousePos(final float mouseX, final float mouseY) - { - Input.mouseX = mouseX; - Input.mouseY = mouseY; - } - - /** - * Assigns a new state to the given mouse button. - * - * @param button - * - The mouse button in question. - * @param pressed - * - True if the mouse button was pressed. False if the mouse button was - * released. - */ - static void setMouseButtonState(final int button, final boolean pressed) - { - mouseButtons[button] = pressed; - } - - /** - * Assigns the amount the mouse wheel was scrolled this frame. - * - * @param delta - * - The scroll delta. - */ - static void setScrollWheelDelta(final float delta) - { - scrollWheelDelta = delta; - } - - /** - * This should be called at the end of the frame. When called, key and mouse - * states are copied from the current frame buffer to the previous frame buffer, - * to allow for delta functions to work as intended. - */ - public static void endFrame() - { - System.arraycopy(keyStates, 0, lastKeyStates, 0, keyStates.length); - System.arraycopy(mouseButtons, 0, lastMouseButtons, 0, mouseButtons.length); - - lastMouseX = mouseX; - lastMouseY = mouseY; - scrollWheelDelta = 0f; - } - - /** - * Checks if the key is currently held down. - * - * @param key - * - The key to check for. - * @return True if the key is being pressed, false otherwise. - */ - public static boolean isKeyDown(final int key) - { - return keyStates[key]; - } - - /** - * Checks if the key was just pressed down this frame. - * - * @param key - * - The key to check for. - * @return True if the key is being pressed and was not pressed on the previous - * frame. False otherwise. - */ - public static boolean isKeyJustDown(final int key) - { - return keyStates[key] && !lastKeyStates[key]; - } - - /** - * Checks if the key was just released this frame. - * - * @param key - * - The key to check for. - * @return True if the key was being pressed last frame and is no longer being - * pressed. False otherwise. - */ - public static boolean isKeyJustUp(final int key) - { - return !keyStates[key] && lastKeyStates[key]; - } - - /** - * Gets the current x position of the mouse. - * - * @return The mouse x pos. - */ - public static float getMouseX() - { - return mouseX; - } - - /** - * Gets the current y position of the mouse. - * - * @return The mouse y pos. - */ - public static float getMouseY() - { - return mouseY; - } - - /** - * Gets the x delta position of the mouse. - * - * @return The mouse delta x. - */ - public static float getMouseDeltaX() - { - return mouseX - lastMouseX; - } - - /** - * Gets the y delta position of the mouse. - * - * @return The mouse delta y. - */ - public static float getMouseDeltaY() - { - return mouseY - lastMouseY; - } - - /** - * Checks if the mouse button is currently held down. - * - * @param button - * - The mouse button to check for. - * @return True if the mouse button is being pressed, false otherwise. - */ - public static boolean isMouseButtonDown(final int button) - { - return mouseButtons[button]; - } - - /** - * Checks if the mouse button was just pressed down this frame. - * - * @param button - * - The button to check for. - * @return True if the mouse button is being pressed and was not pressed on the - * previous frame. False otherwise. - */ - public static boolean isMouseButtonJustDown(final int button) - { - return mouseButtons[button] && !lastMouseButtons[button]; - } - - /** - * Checks if the mouse button was just released this frame. - * - * @param button - * - The button to check for. - * @return True if the mouse button was being pressed last frame and is no - * longer being pressed. False otherwise. - */ - public static boolean isMouseButtonJustUp(final int button) - { - return !mouseButtons[button] && lastMouseButtons[button]; - } - - /** - * Gets the amount the scroll wheel was rotated this frame. - * - * @return The scroll wheel delta. - */ - public static float getScrollWheelDelta() - { - return scrollWheelDelta; - } - - private Input() - {} -} diff --git a/src/main/java/net/whg/we/main/PhysicsPipeline.java b/src/main/java/net/whg/we/main/PhysicsPipeline.java new file mode 100644 index 0000000..126ab3c --- /dev/null +++ b/src/main/java/net/whg/we/main/PhysicsPipeline.java @@ -0,0 +1,57 @@ +package net.whg.we.main; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * The physics pipeline action is an action in charge of triggering physics + * updates each frame based on the physics framerate. + */ +public class PhysicsPipeline implements IPipelineAction +{ + private final List objects = new CopyOnWriteArrayList<>(); + private final Timer timer; + + /** + * Creates a new Physics pipeline action. + * + * @param timer + * - The timer to being this action to. + */ + public PhysicsPipeline(Timer timer) + { + this.timer = timer; + } + + @Override + public void run() + { + while (timer.getPhysicsFrame() < timer.getIdealPhysicsFrame()) + { + timer.incrementPhysicsFrame(); + + for (IFixedUpdateable obj : objects) + obj.fixedUpdate(); + } + } + + @Override + public void enableBehavior(AbstractBehavior behavior) + { + if (behavior instanceof IFixedUpdateable) + objects.add((IFixedUpdateable) behavior); + } + + @Override + public void disableBehavior(AbstractBehavior behavior) + { + if (behavior instanceof IFixedUpdateable) + objects.remove((IFixedUpdateable) behavior); + } + + @Override + public int getPriority() + { + return PipelineConstants.PHYSICS_UPDATES; + } +} diff --git a/src/main/java/net/whg/we/main/PhysicsPipelineAction.java b/src/main/java/net/whg/we/main/PhysicsPipelineAction.java deleted file mode 100644 index f4a49fc..0000000 --- a/src/main/java/net/whg/we/main/PhysicsPipelineAction.java +++ /dev/null @@ -1,36 +0,0 @@ -package net.whg.we.main; - -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; - -public class PhysicsPipelineAction implements IPipelineAction -{ - private final List objects = new CopyOnWriteArrayList<>(); - - @Override - public void run() - { - for (IFixedUpdateable obj : objects) - obj.fixedUpdate(); - } - - @Override - public void enableBehavior(AbstractBehavior behavior) - { - if (behavior instanceof IFixedUpdateable) - objects.add((IFixedUpdateable) behavior); - } - - @Override - public void disableBehavior(AbstractBehavior behavior) - { - if (behavior instanceof IFixedUpdateable) - objects.remove((IFixedUpdateable) behavior); - } - - @Override - public int getPriority() - { - return -10000; - } -} diff --git a/src/main/java/net/whg/we/main/PipelineConstants.java b/src/main/java/net/whg/we/main/PipelineConstants.java new file mode 100644 index 0000000..255e5bd --- /dev/null +++ b/src/main/java/net/whg/we/main/PipelineConstants.java @@ -0,0 +1,189 @@ +package net.whg.we.main; + +/** + * This class contains a set of constants which are used to define the major + * steps within the game loop pipeline. This can be used for determining proper + * offsets for when events should occur. + *

+ * When handling priority values for gameloop actions, direct values, such as + * 23 should not be used. Instead, it is safer and future-proof to + * use values such as PipelineConstants.FRAME_UPDATES + 23. This + * has no effect on preformance and will not break if default values are + * adjusted in the future. + */ +public final class PipelineConstants +{ + /** + * The first call within a frame. Used to preform actions like calculating delta + * time and required phyiscs updates. + *

+ * Value is equal to {@value}. + */ + public static final int CALCULATE_TIMESTAMPS = -1000000; + + /** + * Called early within the frame to update all physics based or other + * time-sensitive updates. This includes things such as player input or AI + * updates. Physics are called on a reliable timer, and are executed multiple + * times per frame, if needed, to catch up, or sometimes ignored completely in + * some frames.s + *

+ * Value is equal to {@value}. + */ + public static final int PHYSICS_UPDATES = -10000; + + /** + * Called to prepare the scene for rendering. One update is called each frame to + * prepare the frame, and only updates events that change each frame. This + * includes actions such as camera transformation, particle effects, or + * interpolating physics updates. This update is also commonly used to indicate + * whether an object will be rendered or not via frustrum culling and distance + * from camera. + *

+ * Value is equal to {@value}. + */ + public static final int FRAME_UPDATES = 0; + + /** + * Called after the update method in order to handle animation updates. This + * usually includes updating skeletal animations and processing particle + * effects. + *

+ * Value is equal to {@value}. + */ + public static final int ANIMATION_UPDATES = 10000; + + /** + * Called after animation updates to post-process skeletal animations. This is + * mainly targeted towards actions such as tweaking foot IK or handling head + * look targets which are applied after the animation pose is determined. + *

+ * Value is equal to {@value}. + */ + public static final int IK_UPDATES = 12500; + + /** + * Called after all updates are preformed on the scene to handle any last minute + * tweaks which would occur before the scene is officially rendered. This is + * most often used for post-update listeners to handle updates which are + * effected by the previous states but effect no others. A common example of + * this is a camera which follows a target, which would be updated in this step + * to ensure the camera follows a smooth path and is called after all other + * entities are in their final render location. + *

+ * Value is equal to {@value}. + */ + public static final int LATE_UPDATES = 20000; + + /** + * This is called at the start of the rendering part of the pipeline to screen + * the screen to render to. This is primarily used to clear the color and depth + * buffer fields. If a skybox is being used, it is rendered during this step. If + * post processing is being used, the buffer frame is prepared during this step. + *

+ * Value is equal to {@value}. + */ + public static final int CLEAR_SCREEN = 29000; + + /** + * After clearing the screen, next the solid objects within the scene are + * rendered. Objects are rendered directly to the output in order from closest + * objects to furthest objects. In a forward rendering path, lighting is applied + * to objects as they are rendered. In a deffered rendering path, objects are + * rendered to the material buffer textures. + *

+ * Value is equal to {@value}. + */ + public static final int RENDER_SOLIDS = 30000; + + /** + * After rendering solids to the screen, solid decals are rendered next. The + * properties of rendering decals is nearly identical to rendering solids. + *

+ * Value is equal to {@value}. + */ + public static final int RENDER_DECALS = 32500; + + /** + * In a deffered rendering pipeline, the lighting updates are applied during + * this phase to draw all lights to the material buffer textures. This step is + * ignored in a forward lighting pipeline. + *

+ * Value is equal to {@value}. + */ + public static final int DEFFERED_RENDERING = 35000; + + /** + * Due to the nature of rendering transparent objects, all transparent objects + * must be rendered together in a single pass. This includes all transparent + * materials and particles. All transparent objects are rendered with a forward + * render pass, regardless of render pipeline, in back-to-front order. + *

+ * Value is equal to {@value}. + */ + public static final int RENDER_TRANSPARENTS = 40000; + + /** + * After the scene is rendered, post processing effects are applied. This + * includes actions such as SSAO or depth blur. If post processing is not + * enabled, nothing happens during this step. + *

+ * Value is equal to {@value}. + */ + public static final int POST_PROCESSING = 50000; + + /** + * To prepare the frame to render UI, the depth buffer is cleared here. + *

+ * Value is equal to {@value}. + */ + public static final int CLEAR_DEPTH = 59000; + + /** + * The UI rendering is all handled in a single pass, in back-to-front order. + *

+ * Value is equal to {@value}. + */ + public static final int RENDER_UI = 60000; + + /** + * At the effective end of the frame, game objects which were marked for removal + * during this frame are removed and disposed. This step is also used for + * general cleanup throughout the scene including disposing unused resources and + * resizing buffers or pools. + *

+ * Value is equal to {@value}. + */ + public static final int DISPOSE_GAMEOBJECTS = 70000; + + /** + * The end frame step is used to trigger some events to mark the frame as + * complete and prepare local buffers for the next frame. A common use-case of + * this is the Input class, which swaps key binding buffers, marking currently + * pressed keys as occuring on the previous frame and preparing to recieve new + * input updates. This step marks the end of frame for all game logic. + *

+ * Value is equal to {@value}. + */ + public static final int ENDFRAME = 80000; + + /** + * Called after all game-logic for a frame is completed, the window is polled + * for new user input events as well as triggering the GPU to swap render + * buffers, pushing the rendered image to the screen. + *

+ * Value is equal to {@value}. + */ + public static final int POLL_WINDOW_EVENTS = 90000; + + /** + * If a framerate cap is enabled, this step serves to sleep the thread as needed + * to enforce that the framerate cap is not exceeded. + *

+ * Value is equal to {@value}. + */ + public static final int FRAMERATE_LIMITER = 100000; + + private PipelineConstants() + {} +} diff --git a/src/main/java/net/whg/we/main/PollEventsPipeline.java b/src/main/java/net/whg/we/main/PollEventsPipeline.java new file mode 100644 index 0000000..d6c8455 --- /dev/null +++ b/src/main/java/net/whg/we/main/PollEventsPipeline.java @@ -0,0 +1,36 @@ +package net.whg.we.main; + +import net.whg.we.window.IWindow; + +/** + * The poll events action is used to trigger a window to poll events and swap + * the render buffer, which needs to be done at the end of every frame in order + * to render the game to the screen. + */ +public class PollEventsPipeline implements ILoopAction +{ + private final IWindow window; + + /** + * Creates a new poll-events action. + * + * @param window + * - The window to poll the events for each frame. + */ + public PollEventsPipeline(IWindow window) + { + this.window = window; + } + + @Override + public void run() + { + window.pollEvents(); + } + + @Override + public int getPriority() + { + return PipelineConstants.POLL_WINDOW_EVENTS; + } +} diff --git a/src/main/java/net/whg/we/main/Screen.java b/src/main/java/net/whg/we/main/Screen.java deleted file mode 100644 index f2d37a9..0000000 --- a/src/main/java/net/whg/we/main/Screen.java +++ /dev/null @@ -1,60 +0,0 @@ -package net.whg.we.main; - -/** - * The screen object is a public static interface which can be used to determine - * various information about the state of the game screen, as well as modidy the - * screen in certain ways. - */ -public final class Screen -{ - private static int width; - private static int height; - - /** - * Assigns the current window size. This should be called every time the window - * is resized. - * - * @param width - * - The width of the window. - * @param height - * - The height of the window. - */ - static void updateWindowSize(int width, int height) - { - Screen.width = width; - Screen.height = height; - } - - /** - * Gets the current width of the window. - * - * @return The width. - */ - public static int getWidth() - { - return width; - } - - /** - * Gets the current height of the window. - * - * @return The height. - */ - public static int getHeight() - { - return height; - } - - /** - * Gets the current aspect of the window. - * - * @return The aspect ratio. - */ - public static float getAspect() - { - return (float) width / height; - } - - private Screen() - {} -} diff --git a/src/main/java/net/whg/we/main/TimerAction.java b/src/main/java/net/whg/we/main/TimerAction.java index 88479fe..711f11d 100644 --- a/src/main/java/net/whg/we/main/TimerAction.java +++ b/src/main/java/net/whg/we/main/TimerAction.java @@ -29,6 +29,6 @@ public void run() @Override public int getPriority() { - return -1000000; + return PipelineConstants.CALCULATE_TIMESTAMPS; } } diff --git a/src/main/java/net/whg/we/main/UpdatePipelineAction.java b/src/main/java/net/whg/we/main/UpdatePipeline.java similarity index 55% rename from src/main/java/net/whg/we/main/UpdatePipelineAction.java rename to src/main/java/net/whg/we/main/UpdatePipeline.java index e182bdc..523c84b 100644 --- a/src/main/java/net/whg/we/main/UpdatePipelineAction.java +++ b/src/main/java/net/whg/we/main/UpdatePipeline.java @@ -3,15 +3,31 @@ import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; -public class UpdatePipelineAction implements IPipelineAction +/** + * The update pipeline is triggered each frame to prepare a scene to be + * rendered, or run logic which needs to be executed every frame. + */ +public class UpdatePipeline implements IPipelineAction { private final List objects = new CopyOnWriteArrayList<>(); + private final Timer timer; + + /** + * Creates a new update pipeline object. + * + * @param timer + * - The timer associated with this update pipeline. + */ + public UpdatePipeline(Timer timer) + { + this.timer = timer; + } @Override public void run() { for (IUpdateable obj : objects) - obj.update(); + obj.update(timer); } @Override @@ -31,6 +47,6 @@ public void disableBehavior(AbstractBehavior behavior) @Override public int getPriority() { - return 0; + return PipelineConstants.FRAME_UPDATES; } } diff --git a/src/main/java/net/whg/we/main/UserControlsUpdater.java b/src/main/java/net/whg/we/main/UserControlsUpdater.java deleted file mode 100644 index 216803d..0000000 --- a/src/main/java/net/whg/we/main/UserControlsUpdater.java +++ /dev/null @@ -1,139 +0,0 @@ -package net.whg.we.main; - -import net.whg.we.window.IWindow; -import net.whg.we.window.IWindowListener; -import net.whg.we.window.WindowSettings; - -/** - * This class acts as a utility class for updating the user control classes in - * the engine, such as Screen and Input. This binds to a window to continously - * update the settings. - */ -public final class UserControlsUpdater implements IWindowListener -{ - private static UserControlsUpdater updater = new UserControlsUpdater(); - - /** - * Binds this updater to the current window. Replaces the binding with previous - * window, if previously binded. This updater is automaically unbound when the - * window is destroyed. - * - * @param window - * - The window to bind with, or null to disable current bindings. - */ - public static void bind(IWindow window) - { - updater.setWindow(window); - } - - /** - * Gets the window which the updater is currently bound to. - * - * @return The window, or null if no window is bound. - */ - public static IWindow getBoundWindow() - { - return updater.getWindow(); - } - - private IWindow window; - - private UserControlsUpdater() - {} - - /** - * Assigns the target window to bind to. - * - * @param window - * - The new target window. - */ - private void setWindow(IWindow window) - { - if (this.window == window) - return; - - if (this.window != null) - this.window.removeWindowListener(this); - - this.window = window; - - if (this.window != null) - { - this.window.addWindowListener(this); - - WindowSettings settings = this.window.getProperties(); - Screen.updateWindowSize(settings.getWidth(), settings.getHeight()); - } - } - - /** - * Gets the window which this updater is currently bound to. - * - * @return The window, or null if no window is bound. - */ - private IWindow getWindow() - { - return window; - } - - @Override - public void onWindowUpdated(IWindow window) - { - WindowSettings settings = window.getProperties(); - onWindowResized(window, settings.getWidth(), settings.getHeight()); - } - - @Override - public void onWindowResized(IWindow window, int width, int height) - { - Screen.updateWindowSize(width, height); - } - - @Override - public void onWindowDestroyed(IWindow window) - { - setWindow(null); - } - - @Override - public void onWindowRequestClose(IWindow window) - { - // Nothing to do. - } - - @Override - public void onMouseMove(IWindow window, float newX, float newY) - { - Input.setMousePos(newX, newY); - } - - @Override - public void onKeyPressed(IWindow window, int keyCode) - { - Input.setKeyState(keyCode, true); - } - - @Override - public void onKeyReleased(IWindow window, int keyCode) - { - Input.setKeyState(keyCode, false); - } - - @Override - public void onMousePressed(IWindow window, int mouseButton) - { - Input.setMouseButtonState(mouseButton, true); - } - - @Override - public void onMouseReleased(IWindow window, int mouseButton) - { - Input.setMouseButtonState(mouseButton, false); - } - - @Override - public void onMouseWheel(IWindow window, float scrollX, float scrollY) - { - Input.setScrollWheelDelta(scrollY); - } -} diff --git a/src/main/java/net/whg/we/rendering/Camera.java b/src/main/java/net/whg/we/rendering/Camera.java index 5243528..504775f 100644 --- a/src/main/java/net/whg/we/rendering/Camera.java +++ b/src/main/java/net/whg/we/rendering/Camera.java @@ -1,8 +1,8 @@ package net.whg.we.rendering; import org.joml.Matrix4f; -import net.whg.we.main.Screen; import net.whg.we.main.Transform3D; +import net.whg.we.window.Screen; /** * The camera is the object in charge of determing the projection and view @@ -10,17 +10,39 @@ */ public class Camera { - private final Transform3D transform = new Transform3D(); private final Matrix4f projectionMatrix = new Matrix4f(); + private final Transform3D transform; + private final Screen screen; private float fov = (float) Math.toRadians(90f); private float nearClip = 0.1f; private float farClip = 1000f; /** * Creates a new camera object with the default projection matrix. + * + * @param screen + * - The screen this camera pulls information from. + */ + public Camera(Screen screen) + { + this(new Transform3D(), screen); + } + + /** + * Creates a new camera object with the default projection matrix. The transform + * for this camera is maintained externally, such as being attached to a game + * object, and will return the given transform when {@link #getTransform()} is + * called. + * + * @param transform + * - The transform this camera should use. + * @param screen + * - The screen this camera pulls information from. */ - public Camera() + public Camera(Transform3D transform, Screen screen) { + this.transform = transform; + this.screen = screen; rebuildProjectionMatrix(); } @@ -29,7 +51,7 @@ public Camera() */ private void rebuildProjectionMatrix() { - float aspect = Screen.getAspect(); + float aspect = screen.getAspect(); projectionMatrix.identity(); projectionMatrix.perspective(fov, aspect, nearClip, farClip); diff --git a/src/main/java/net/whg/we/main/RenderBehavior.java b/src/main/java/net/whg/we/rendering/RenderBehavior.java similarity index 95% rename from src/main/java/net/whg/we/main/RenderBehavior.java rename to src/main/java/net/whg/we/rendering/RenderBehavior.java index e6c738e..162f32c 100644 --- a/src/main/java/net/whg/we/main/RenderBehavior.java +++ b/src/main/java/net/whg/we/rendering/RenderBehavior.java @@ -1,9 +1,7 @@ -package net.whg.we.main; +package net.whg.we.rendering; import org.joml.Matrix4f; -import net.whg.we.rendering.Camera; -import net.whg.we.rendering.IMesh; -import net.whg.we.rendering.Material; +import net.whg.we.main.AbstractBehavior; /** * This behavior is used as a method for rendering a mesh to the scene. When diff --git a/src/main/java/net/whg/we/main/RenderPipelineAction.java b/src/main/java/net/whg/we/rendering/RenderPipeline.java similarity index 72% rename from src/main/java/net/whg/we/main/RenderPipelineAction.java rename to src/main/java/net/whg/we/rendering/RenderPipeline.java index 386ffe6..c059a66 100644 --- a/src/main/java/net/whg/we/main/RenderPipelineAction.java +++ b/src/main/java/net/whg/we/rendering/RenderPipeline.java @@ -1,20 +1,41 @@ -package net.whg.we.main; +package net.whg.we.rendering; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import net.whg.we.rendering.Camera; +import net.whg.we.main.AbstractBehavior; +import net.whg.we.main.IPipelineAction; +import net.whg.we.main.PipelineConstants; /** * The renderer pipeline action is used to render elements within a scene. By * default, all behaviors which extend {@link RenderBehavior} are used. */ -public class RenderPipelineAction implements IPipelineAction +public class RenderPipeline implements IPipelineAction { private final List renderedObjects = new ArrayList<>(); private final List renderedObjectsReadOnly = Collections.unmodifiableList(renderedObjects); private Camera camera; + /** + * Creates a new render pipeline action. + */ + public RenderPipeline() + { + this(null); + } + + /** + * Creates a new render pipeline action and initializes it with a camera. + * + * @param camera + * - The camera to attach to this render pipeline action. + */ + public RenderPipeline(Camera camera) + { + this.camera = camera; + } + @Override public void run() { @@ -74,6 +95,6 @@ public List renderBehaviors() @Override public int getPriority() { - return 30000; + return PipelineConstants.RENDER_SOLIDS; } } diff --git a/src/main/java/net/whg/we/rendering/ScreenClearPipeline.java b/src/main/java/net/whg/we/rendering/ScreenClearPipeline.java new file mode 100644 index 0000000..fd60f98 --- /dev/null +++ b/src/main/java/net/whg/we/rendering/ScreenClearPipeline.java @@ -0,0 +1,36 @@ +package net.whg.we.rendering; + +import net.whg.we.main.IPipelineAction; +import net.whg.we.main.PipelineConstants; + +/** + * The screen clear pipeline simply clears the screen at the begining of the + * render phase of a frame. + */ +public class ScreenClearPipeline implements IPipelineAction +{ + private final IScreenClearHandler screenClear; + + /** + * Creates a new screen clear pipeline object. + * + * @param screenClear + * - The screen clear handler to trigger each frame. + */ + public ScreenClearPipeline(IScreenClearHandler screenClear) + { + this.screenClear = screenClear; + } + + @Override + public void run() + { + screenClear.clearScreen(); + } + + @Override + public int getPriority() + { + return PipelineConstants.CLEAR_SCREEN; + } +} diff --git a/src/main/java/net/whg/we/window/Input.java b/src/main/java/net/whg/we/window/Input.java new file mode 100644 index 0000000..7b8aaf5 --- /dev/null +++ b/src/main/java/net/whg/we/window/Input.java @@ -0,0 +1,313 @@ +package net.whg.we.window; + +import net.whg.we.util.IDisposable; + +/** + * This input class is a reference for the current key and mouse states for the + * focused window. This can be used to check for mouse inputs and key presses as + * they occur. + */ +public class Input implements IDisposable +{ + private static final String INPUT_DISPOSED = "Input already disposed!"; + + /** + * The largest key code input provided by GLFW. Used for making sure enough + * memory is allocated to store all key states. + */ + private static final int MAX_INPUT_KEY_CODE = 348; + + /** + * The largest mouse button input provided by GLFW. Used for making sure enough + * memory is allocated to store all mouse button states. + */ + private static final int MAX_INPUT_MOUSE_BUTTON = 7; + + /** + * The input listener is a helper class which can be used to bind to a window to + * listen for events it triggers. + */ + private class InputListener extends IWindowAdapter + { + @Override + public void onMouseMove(IWindow window, float newX, float newY) + { + Input.this.mouseX = newX; + Input.this.mouseY = newY; + } + + @Override + public void onKeyPressed(IWindow window, int keyCode) + { + Input.this.keyStates[keyCode] = true; + } + + @Override + public void onKeyReleased(IWindow window, int keyCode) + { + Input.this.keyStates[keyCode] = false; + } + + @Override + public void onMouseWheel(IWindow window, float scrollX, float scrollY) + { + Input.this.scrollWheelDelta = scrollY; + } + + @Override + public void onMousePressed(IWindow window, int mouseButton) + { + Input.this.mouseButtons[mouseButton] = true; + } + + @Override + public void onMouseReleased(IWindow window, int mouseButton) + { + Input.this.mouseButtons[mouseButton] = false; + } + } + + private final InputListener listener = new InputListener(); + private final boolean[] keyStates = new boolean[MAX_INPUT_KEY_CODE + 1]; + private final boolean[] lastKeyStates = new boolean[MAX_INPUT_KEY_CODE + 1]; + private final boolean[] mouseButtons = new boolean[MAX_INPUT_MOUSE_BUTTON + 1]; + private final boolean[] lastMouseButtons = new boolean[MAX_INPUT_MOUSE_BUTTON + 1]; + private IWindow window; + private float mouseX; + private float mouseY; + private float lastMouseX; + private float lastMouseY; + private float scrollWheelDelta; + private boolean disposed; + + /** + * Creates a new input object, and binds a listener to the given window. + * + * @param window + * - The window to bind to. + */ + public Input(IWindow window) + { + this.window = window; + window.addWindowListener(listener); + } + + /** + * This should be called at the end of the frame. When called, key and mouse + * states are copied from the current frame buffer to the previous frame buffer, + * to allow for delta functions to work as intended. + * + * @throws IllegalStateException + * If input is already disposed. + */ + public void endFrame() + { + if (isDisposed()) + throw new IllegalStateException(INPUT_DISPOSED); + + System.arraycopy(keyStates, 0, lastKeyStates, 0, keyStates.length); + System.arraycopy(mouseButtons, 0, lastMouseButtons, 0, mouseButtons.length); + + lastMouseX = mouseX; + lastMouseY = mouseY; + scrollWheelDelta = 0f; + } + + /** + * Checks if the key is currently held down. + * + * @param key + * - The key to check for. + * @return True if the key is being pressed, false otherwise. + * @throws IllegalStateException + * If input is already disposed. + */ + public boolean isKeyDown(final int key) + { + if (isDisposed()) + throw new IllegalStateException(INPUT_DISPOSED); + + return keyStates[key]; + } + + /** + * Checks if the key was just pressed down this frame. + * + * @param key + * - The key to check for. + * @return True if the key is being pressed and was not pressed on the previous + * frame. False otherwise. + * @throws IllegalStateException + * If input is already disposed. + */ + public boolean isKeyJustDown(final int key) + { + if (isDisposed()) + throw new IllegalStateException(INPUT_DISPOSED); + + return keyStates[key] && !lastKeyStates[key]; + } + + /** + * Checks if the key was just released this frame. + * + * @param key + * - The key to check for. + * @return True if the key was being pressed last frame and is no longer being + * pressed. False otherwise. + * @throws IllegalStateException + * If input is already disposed. + */ + public boolean isKeyJustUp(final int key) + { + if (isDisposed()) + throw new IllegalStateException(INPUT_DISPOSED); + + return !keyStates[key] && lastKeyStates[key]; + } + + /** + * Gets the current x position of the mouse. + * + * @return The mouse x pos. + * @throws IllegalStateException + * If input is already disposed. + */ + public float getMouseX() + { + if (isDisposed()) + throw new IllegalStateException(INPUT_DISPOSED); + + return mouseX; + } + + /** + * Gets the current y position of the mouse. + * + * @return The mouse y pos. + * @throws IllegalStateException + * If input is already disposed. + */ + public float getMouseY() + { + if (isDisposed()) + throw new IllegalStateException(INPUT_DISPOSED); + + return mouseY; + } + + /** + * Gets the x delta position of the mouse. + * + * @return The mouse delta x. + * @throws IllegalStateException + * If input is already disposed. + */ + public float getMouseDeltaX() + { + if (isDisposed()) + throw new IllegalStateException(INPUT_DISPOSED); + + return mouseX - lastMouseX; + } + + /** + * Gets the y delta position of the mouse. + * + * @return The mouse delta y. + * @throws IllegalStateException + * If input is already disposed. + */ + public float getMouseDeltaY() + { + if (isDisposed()) + throw new IllegalStateException(INPUT_DISPOSED); + + return mouseY - lastMouseY; + } + + /** + * Checks if the mouse button is currently held down. + * + * @param button + * - The mouse button to check for. + * @return True if the mouse button is being pressed, false otherwise. + * @throws IllegalStateException + * If input is already disposed. + */ + public boolean isMouseButtonDown(final int button) + { + if (isDisposed()) + throw new IllegalStateException(INPUT_DISPOSED); + + return mouseButtons[button]; + } + + /** + * Checks if the mouse button was just pressed down this frame. + * + * @param button + * - The button to check for. + * @return True if the mouse button is being pressed and was not pressed on the + * previous frame. False otherwise. + * @throws IllegalStateException + * If input is already disposed. + */ + public boolean isMouseButtonJustDown(final int button) + { + if (isDisposed()) + throw new IllegalStateException(INPUT_DISPOSED); + + return mouseButtons[button] && !lastMouseButtons[button]; + } + + /** + * Checks if the mouse button was just released this frame. + * + * @param button + * - The button to check for. + * @return True if the mouse button was being pressed last frame and is no + * longer being pressed. False otherwise. + * @throws IllegalStateException + * If input is already disposed. + */ + public boolean isMouseButtonJustUp(final int button) + { + if (isDisposed()) + throw new IllegalStateException(INPUT_DISPOSED); + + return !mouseButtons[button] && lastMouseButtons[button]; + } + + /** + * Gets the amount the scroll wheel was rotated this frame. + * + * @return The scroll wheel delta. + * @throws IllegalStateException + * If input is already disposed. + */ + public float getScrollWheelDelta() + { + if (isDisposed()) + throw new IllegalStateException(INPUT_DISPOSED); + + return scrollWheelDelta; + } + + @Override + public void dispose() + { + if (isDisposed()) + return; + + disposed = true; + window.removeWindowListener(listener); + window = null; + } + + @Override + public boolean isDisposed() + { + return disposed; + } +} diff --git a/src/main/java/net/whg/we/window/InputEndFrameAction.java b/src/main/java/net/whg/we/window/InputEndFrameAction.java new file mode 100644 index 0000000..abfb5de --- /dev/null +++ b/src/main/java/net/whg/we/window/InputEndFrameAction.java @@ -0,0 +1,31 @@ +package net.whg.we.window; + +import net.whg.we.main.ILoopAction; +import net.whg.we.main.PipelineConstants; +import net.whg.we.window.Input; + +/** + * This action triggers the end-frame method for input objects near the end of + * the loop to prepare the object for recieving input updates. + */ +public class InputEndFrameAction implements ILoopAction +{ + private final Input input; + + public InputEndFrameAction(Input input) + { + this.input = input; + } + + @Override + public void run() + { + input.endFrame(); + } + + @Override + public int getPriority() + { + return PipelineConstants.ENDFRAME; + } +} diff --git a/src/main/java/net/whg/we/window/Screen.java b/src/main/java/net/whg/we/window/Screen.java new file mode 100644 index 0000000..1de2697 --- /dev/null +++ b/src/main/java/net/whg/we/window/Screen.java @@ -0,0 +1,111 @@ +package net.whg.we.window; + +import net.whg.we.util.IDisposable; + +/** + * The screen object is an object which can be used to determine various + * information about the state of the game screen, as well as modidy the screen + * in certain ways. Each screen is bound to a single window. + */ +public class Screen implements IDisposable +{ + private static final String SCREEN_DISPOSED = "Screen already disposed!"; + + /** + * A private listener class which recieved events from the window to store + * within the screen object. + */ + private class ScreenListener extends IWindowAdapter + { + @Override + public void onWindowResized(IWindow window, int width, int height) + { + Screen.this.width = width; + Screen.this.height = height; + } + + @Override + public void onWindowUpdated(IWindow window) + { + WindowSettings settings = window.getProperties(); + Screen.this.width = settings.getWidth(); + Screen.this.height = settings.getHeight(); + } + } + + private final IWindowListener listener = new ScreenListener(); + private IWindow window; + private int width; + private int height; + private boolean disposed; + + /** + * Creates a new screen object and binds to the given window. + * + * @param window + * - The window to bind to. + */ + public Screen(IWindow window) + { + this.window = window; + window.addWindowListener(listener); + + listener.onWindowUpdated(window); + } + + /** + * Gets the current width of the window. + * + * @return The width. + */ + public int getWidth() + { + if (isDisposed()) + throw new IllegalStateException(SCREEN_DISPOSED); + + return width; + } + + /** + * Gets the current height of the window. + * + * @return The height. + */ + public int getHeight() + { + if (isDisposed()) + throw new IllegalStateException(SCREEN_DISPOSED); + + return height; + } + + /** + * Gets the current aspect of the window. + * + * @return The aspect ratio. + */ + public float getAspect() + { + if (isDisposed()) + throw new IllegalStateException(SCREEN_DISPOSED); + + return (float) width / height; + } + + @Override + public void dispose() + { + if (isDisposed()) + return; + + disposed = true; + window.removeWindowListener(listener); + window = null; + } + + @Override + public boolean isDisposed() + { + return disposed; + } +} diff --git a/src/main/java/net/whg/we/window/WindowCloseHandler.java b/src/main/java/net/whg/we/window/WindowCloseHandler.java new file mode 100644 index 0000000..07ebbe1 --- /dev/null +++ b/src/main/java/net/whg/we/window/WindowCloseHandler.java @@ -0,0 +1,79 @@ +package net.whg.we.window; + +import net.whg.we.main.GameLoop; +import net.whg.we.util.IDisposable; + +/** + * This class is a simple utility which listens for when a window close request + * is triggered and stops the connected game loop, allowing the window to close. + *

+ * The window binding can be removed by disposing this object. This object will + * also automatially be disposed after stopping the game loop. + */ +public class WindowCloseHandler implements IDisposable +{ + /** + * Creates a new window close handler. This will automatically bind the listener + * to the window. + * + * @param window + * - The window to bind to. + * @param gameLoop + * - The game loop to stop when the window requests to close. + */ + public static WindowCloseHandler bindToWindow(IWindow window, GameLoop gameLoop) + { + return new WindowCloseHandler(window, gameLoop); + } + + /** + * A simple listener which listens for a window close request. + */ + private class WindowCloseListener extends IWindowAdapter + { + @Override + public void onWindowRequestClose(IWindow window) + { + gameLoop.stop(); + dispose(); + } + } + + private final WindowCloseListener listener = new WindowCloseListener(); + private final IWindow window; + private final GameLoop gameLoop; + private boolean disposed; + + /** + * Creates a new window close handler. This constructor handles the window + * binding automatically. + * + * @param window + * - The window to bind to. + * @param gameLoop + * - The game loop to stop when the window requests to close. + */ + private WindowCloseHandler(IWindow window, GameLoop gameLoop) + { + this.window = window; + this.gameLoop = gameLoop; + + window.addWindowListener(listener); + } + + @Override + public void dispose() + { + if (isDisposed()) + return; + + disposed = true; + window.removeWindowListener(listener); + } + + @Override + public boolean isDisposed() + { + return disposed; + } +} diff --git a/src/test/java/demo/SpinningCubeDemo.java b/src/test/java/demo/SpinningCubeDemo.java new file mode 100644 index 0000000..6684903 --- /dev/null +++ b/src/test/java/demo/SpinningCubeDemo.java @@ -0,0 +1,282 @@ +package demo; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; +import org.joml.Quaternionf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import net.whg.we.external.AssimpAPI; +import net.whg.we.external.GlfwApi; +import net.whg.we.external.OpenGLApi; +import net.whg.we.external.TimeSupplierApi; +import net.whg.we.main.AbstractBehavior; +import net.whg.we.main.GameObject; +import net.whg.we.main.IUpdateable; +import net.whg.we.main.PollEventsPipeline; +import net.whg.we.main.Scene; +import net.whg.we.main.SceneGameLoop; +import net.whg.we.main.Timer; +import net.whg.we.main.TimerAction; +import net.whg.we.main.UpdatePipeline; +import net.whg.we.rendering.Camera; +import net.whg.we.rendering.Color; +import net.whg.we.rendering.IMesh; +import net.whg.we.rendering.IRenderingEngine; +import net.whg.we.rendering.IScreenClearHandler; +import net.whg.we.rendering.IShader; +import net.whg.we.rendering.ITexture; +import net.whg.we.rendering.Material; +import net.whg.we.rendering.RawShaderCode; +import net.whg.we.rendering.RenderBehavior; +import net.whg.we.rendering.RenderPipeline; +import net.whg.we.rendering.ScreenClearPipeline; +import net.whg.we.rendering.TextureData; +import net.whg.we.rendering.VertexData; +import net.whg.we.rendering.TextureData.SampleMode; +import net.whg.we.rendering.opengl.OpenGLRenderingEngine; +import net.whg.we.resource.ModelLoader; +import net.whg.we.resource.Resource; +import net.whg.we.resource.TextureLoader; +import net.whg.we.window.IWindow; +import net.whg.we.window.Screen; +import net.whg.we.window.WindowCloseHandler; +import net.whg.we.window.WindowSettings; +import net.whg.we.window.glfw.GlfwWindow; + +/** + * The spinning cube demo shows all of the core elements required to create a + * simple scene set up. This includes loading a window, handling timing, working + * with a pipeline, creating behaviors, updating a scene, loading resources, and + * rendering a scene. + *

+ * In this demo, you can see a small textured cube which was loaded from file, + * placed into a scene and is set rotating endlessly. The purpose of this demo + * is to show how a game would normally be set up to allow for elements to be + * added or configured. + * + * @author TheDudeFromCI + */ +public class SpinningCubeDemo +{ + private static final Logger logger = LoggerFactory.getLogger(SpinningCubeDemo.class); + + /** + * Launches the spinning cube demo. + * + * @param args + * - Does nothing + * @throws IOException + * If the required resource files could not be found. + */ + public static void main(String[] args) throws IOException + { + logger.info("Running Spinning Cube demo."); + + // The window settings object can be used to configure how the window should be + // displayed. This includes things like title, size, fullscreen, vSync, etc. + WindowSettings windowSettings = new WindowSettings(); + windowSettings.setTitle("Spinning Cubes Demo"); + + // The rendering engine is used to actually render the game. Here, we're loading + // the OpenGL 3.3 rendering engine. + IRenderingEngine renderingEngine = new OpenGLRenderingEngine(new OpenGLApi()); + + // Creates and displays the window. This window uses the GLFW window handling + // framework. The window will initialize the rendering engine for us here. + IWindow window = new GlfwWindow(new GlfwApi(), renderingEngine, windowSettings); + + // Now we can start building our scene. A scene is simply a collection of game + // objects which are active at a given time. This could mean they are being + // rendered to the screen, or are simply running logic in the background. A + // scene is needed to maintain what's active and what's not. Scenes operate on a + // pipeline workflow, so we'll need to create the pipeline now. + Scene scene = new Scene(); + + // As we can to clear the screen each frame, to draw a new image, we'll add a + // screen clear pipeline action to the scene. This will tell the scene to clear + // the screen each frame at the correct time. We'll also confiugre the clear + // color to be a dark blue color. + IScreenClearHandler screenClear = renderingEngine.getScreenClearHandler(); + screenClear.setClearColor(new Color(0.2f, 0.2f, 0.5f)); + scene.addPipelineAction(new ScreenClearPipeline(screenClear)); + + // Since we want to render our scene, we need to add a render pipeline action to + // it. The render pipeline can be initialized with a camera, so we'll go ahead + // and do that here. We'll also offset the position of the camera to see the + // cube better. + Camera camera = new Camera(new Screen(window)); + scene.addPipelineAction(new RenderPipeline(camera)); + camera.getTransform() + .setPosition(0f, 0f, 3f); + + // A game loop is the more important element. This loop is in charge of handling + // running the events each frame which are required to run the game. A + // SceneGameLoop is an extention of the game loop which will automatically + // handle scenes and pipelines as well. + SceneGameLoop gameLoop = new SceneGameLoop(); + gameLoop.addScene(scene); + + // A lot of updates within a game tend to require a timer to work properly. In + // our case, the time is needed each frame in order to update the rotation of + // the cube. So we'll add that here. Notice this is added to the game loop + // itself, not the scene. + Timer timer = new Timer(new TimeSupplierApi()); + gameLoop.addAction(new TimerAction(timer)); + + // In order to spin the cube, it will need to be updated every frame. To trigger + // these updates, we'll need to add an update pipeline action to the scene. We + // can use the timer object we created eariler to initialize the pipeline with. + scene.addPipelineAction(new UpdatePipeline(timer)); + + // When working with a window, it's extremely important to poll the window + // events at the end of each frame in order to actually display anything on + // screen or recieve any window events. We can add this helper here to do that + // for us. Once again, this is added to the game loop and not the scene. + gameLoop.addAction(new PollEventsPipeline(window)); + + // When the user clicks the close button on the window, the window sends out a + // window close request. This utility will trigger the game loop to stop once + // the window close request is made. + WindowCloseHandler.bindToWindow(window, gameLoop); + + // Finally, let's create the cube game object, and add it to the scene. + scene.addGameObject(buildCube(renderingEngine)); + + // Now that the scene is set up, we're ready to start the game loop. Since we're + // working with a timer, we want to start the timer right before starting the + // game loop to make sure all the timing is correct. + timer.startTimer(); + gameLoop.loop(); + + // At the game loop is stopped, we can dispose the window and let the program + // exit peacefully. + window.dispose(); + } + + /** + * This function loads all the text from a file and returns it as a string. + * + * @param file + * - The file to load. + * @return The text within the file. + * @throws IOException + * If there was an error while loading the file. + */ + private static String loadText(File file) throws IOException + { + return new String(Files.readAllBytes(Paths.get(file.getAbsolutePath())), StandardCharsets.UTF_8); + } + + /** + * This function loads all resources required by the spinning cube, and returns + * it as a game object to be added to the scene. + * + * @param renderingEngine + * - The rendering engine, to allocate resources with. + * @return The spinning cube game object. + * @throws IOException + * If the resources could not be loaded. + */ + private static GameObject buildCube(IRenderingEngine renderingEngine) throws IOException + { + // Just laying out the file paths to where each resource is located. + File cubeFile = new File("src/test/res/cube.obj"); + File diffuseVertFile = new File("src/test/res/diffuse.vert"); + File diffuseFragFile = new File("src/test/res/diffuse.frag"); + File grassFile = new File("src/test/res/grass.png"); + + // First, let's load the mesh. This one is a bit messy looking, so let's break + // it down. The model loader is an object which can be used to load 3D model + // files. We'll use this here to load the cube file. + ModelLoader modelLoader = new ModelLoader(new AssimpAPI()); + + // When the model loader loads a file, a list of resources are returned. These + // are all the resources the model loader was able to pull from the file. + // (I.e. mesh, skeleton, materials, etc) In our case, the file only contains + // mesh data, so we can extract the first resource from the list and pull the + // data as vertex data. + List resources = modelLoader.loadScene(cubeFile); + VertexData vertexData = (VertexData) resources.get(0) + .getData(); + + // Now that we have the vertex data, we can compile it into a mesh. + IMesh mesh = renderingEngine.createMesh(); + mesh.update(vertexData); + + // To load the shader, we just need the vertex shader code and the fragment + // shader code. We can load the text from those files, and place them into a raw + // shader code object to pass to the shader. + RawShaderCode shaderCode = new RawShaderCode(loadText(diffuseVertFile), loadText(diffuseFragFile)); + + // Like the mesh, create the shader and compile it using the shader code. + IShader shader = renderingEngine.createShader(); + shader.compile(shaderCode); + + // Now for the texture. We can load a texture in the same manner as loading a + // model, using the TextureLoader object. We also want to change the sampling + // mode to NEAREST, to keep that "pixel-y" look. + TextureData textureData = TextureLoader.loadTexture(grassFile); + textureData.setSampleMode(SampleMode.NEAREST); + + // Now we can compile the texture data into a texture. + ITexture texture = renderingEngine.createTexture(); + texture.update(textureData); + + // After gathering both the shader and the textures, we can use those to create + // a material which represents our grass. Here, "diffuse" is the name of the + // texture within the shader code, and corresponds to the grass texture. + Material material = new Material(shader); + material.setTextures(new ITexture[] {texture}, new String[] {"diffuse"}); + + // All the pieces are set up! Now lets create the game object which represents + // the cube. + GameObject gameObject = new GameObject(); + gameObject.setName("Cube"); + + // A Behavior is a piece of logic which is attached to a game object to change + // it's behavior. A render behavoir is an example of that which can be used to + // get an object to render. You simplely need to provide it with a mesh and a + // material to work with. Once added to the cube, the cube should be appear on + // screen. + RenderBehavior renderBehavior = new RenderBehavior(); + renderBehavior.setMesh(mesh); + renderBehavior.setMaterial(material); + gameObject.addBehavior(renderBehavior); + + // We also want to add another behavior for making our cube spin. The custom + // class for that logic can be seen below. Here we just create a new instance + // and add it to the cube. + gameObject.addBehavior(new SpinCube()); + + return gameObject; + } + + /** + * This class contains a simple set of logic which will rotate the cube forever, + * updating the rotation each frame before rendering. + */ + private static class SpinCube extends AbstractBehavior implements IUpdateable + { + @Override + public void update(Timer timer) + { + // Get the rotation component of the game object's transform. + Quaternionf rot = getGameObject().getTransform() + .getRotation(); + + // Get the amount of time, in seconds, that passed since the timer was started. + // (At the beginning of the game loop.) + float t = (float) timer.getElapsedTime(); + + // Reset the rotation, and apply the new rotation values to it. + rot.identity(); + rot.rotateLocalX(t * 2.2369f); + rot.rotateLocalY(t * 1.4562f); + rot.rotateLocalZ(t * 0.2123f); + } + } +} diff --git a/src/test/java/manual/Scene1Example.java b/src/test/java/manual/Scene1Example.java deleted file mode 100644 index 456046f..0000000 --- a/src/test/java/manual/Scene1Example.java +++ /dev/null @@ -1,151 +0,0 @@ -package manual; - -import java.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.List; -import org.joml.Quaternionf; -import net.whg.we.external.AssimpAPI; -import net.whg.we.external.GlfwApi; -import net.whg.we.external.OpenGLApi; -import net.whg.we.main.GameLoop; -import net.whg.we.main.GameObject; -import net.whg.we.main.Input; -import net.whg.we.main.RenderBehavior; -import net.whg.we.main.Scene; -import net.whg.we.main.UserControlsUpdater; -import net.whg.we.rendering.Camera; -import net.whg.we.rendering.Color; -import net.whg.we.rendering.CullingMode; -import net.whg.we.rendering.IMesh; -import net.whg.we.rendering.IRenderingEngine; -import net.whg.we.rendering.IScreenClearHandler; -import net.whg.we.rendering.IShader; -import net.whg.we.rendering.ITexture; -import net.whg.we.rendering.Material; -import net.whg.we.rendering.RawShaderCode; -import net.whg.we.rendering.TextureData; -import net.whg.we.rendering.VertexData; -import net.whg.we.rendering.TextureData.SampleMode; -import net.whg.we.rendering.opengl.IOpenGL; -import net.whg.we.rendering.opengl.OpenGLRenderingEngine; -import net.whg.we.resource.ModelLoader; -import net.whg.we.resource.Resource; -import net.whg.we.resource.TextureLoader; -import net.whg.we.resource.assimp.IAssimp; -import net.whg.we.window.IWindow; -import net.whg.we.window.IWindowAdapter; -import net.whg.we.window.WindowSettings; -import net.whg.we.window.glfw.GlfwWindow; -import net.whg.we.window.glfw.IGlfw; - -public class Scene1Example -{ - public static void main(String[] args) throws IOException - { - IGlfw glfw = new GlfwApi(); - IOpenGL opengl = new OpenGLApi(true); - IAssimp assimp = new AssimpAPI(); - - IRenderingEngine renderingEngine = new OpenGLRenderingEngine(opengl); - WindowSettings windowSettings = new WindowSettings(); - IWindow window = new GlfwWindow(glfw, renderingEngine, windowSettings); - - UserControlsUpdater.bind(window); - - IScreenClearHandler screenClear = renderingEngine.getScreenClearHandler(); - screenClear.setClearColor(new Color(0.15f, 0.15f, 0.15f)); - - ModelLoader modelLoader = new ModelLoader(assimp); - - List resources = modelLoader.loadScene(new File("src/test/res/cube.obj")); - VertexData cubeData = (VertexData) resources.get(0) - .getData(); - String vertShader = - new String(Files.readAllBytes(Paths.get("src/test/res/normal_shader.vert")), StandardCharsets.UTF_8); - String fragShader = - new String(Files.readAllBytes(Paths.get("src/test/res/normal_shader.frag")), StandardCharsets.UTF_8); - - Camera camera = new Camera(); - camera.getTransform() - .setPosition(0, 0, 5); - - IMesh mesh = renderingEngine.createMesh(); - mesh.update(cubeData); - - IShader shader = renderingEngine.createShader(); - shader.compile(new RawShaderCode(vertShader, fragShader)); - - TextureData textureData = TextureLoader.loadTexture(new File("src/test/res/grass.png")); - textureData.setSampleMode(SampleMode.NEAREST); - textureData.setMipmap(true); - ITexture texture = renderingEngine.createTexture(); - texture.update(textureData); - - renderingEngine.setCullingMode(CullingMode.NONE); - - Material material = new Material(shader); - material.setTextures(new ITexture[] {texture}, new String[] {"diffuse"}); - - GameObject cube = new GameObject(); - RenderBehavior renderer = new RenderBehavior(); - renderer.setMesh(mesh); - renderer.setMaterial(material); - cube.addBehavior(renderer); - - Scene scene = new Scene(); - scene.addGameObject(cube); - - cube.getTransform() - .setRotation(new Quaternionf(0.5f, 0.5f, 0f, 1f)); - - GameLoop gameLoop = new GameLoop(); - - gameLoop.addAction(() -> - { - if (Input.isMouseButtonDown(0)) - { - final float s = 0.01f; - float dx = Input.getMouseDeltaX() * s; - float dy = Input.getMouseDeltaY() * s; - - cube.getTransform() - .getRotation() - .rotateX(dy) - .rotateY(dx); - } - - if (Input.getScrollWheelDelta() != 0) - { - cube.getTransform() - .setSize(cube.getTransform() - .getSize().x - * (float) Math.pow(1.1f, -Input.getScrollWheelDelta())); - } - }); - gameLoop.addAction(() -> screenClear.clearScreen()); - // gameLoop.addAction(() -> scene.getRenderer() - // .render(camera)); - gameLoop.addAction(() -> Input.endFrame()); - gameLoop.addAction(() -> window.pollEvents()); - - window.addWindowListener(new IWindowAdapter() - { - @Override - public void onWindowRequestClose(IWindow window) - { - gameLoop.stop(); - } - }); - - gameLoop.loop(); - - mesh.dispose(); - shader.dispose(); - texture.dispose(); - - window.dispose(); - } -} diff --git a/src/test/java/unit/CameraTest.java b/src/test/java/unit/CameraTest.java index a01a020..2edd0ea 100644 --- a/src/test/java/unit/CameraTest.java +++ b/src/test/java/unit/CameraTest.java @@ -2,18 +2,22 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import org.joml.Matrix4f; import org.joml.Quaternionf; import org.joml.Vector3f; import org.junit.Test; +import net.whg.we.main.Transform3D; import net.whg.we.rendering.Camera; +import net.whg.we.window.Screen; public class CameraTest { @Test public void defaultProperties() { - Camera camera = new Camera(); + Camera camera = new Camera(mock(Screen.class)); assertEquals(0.1f, camera.getNearClip(), 0f); assertEquals(1000f, camera.getFarClip(), 0f); @@ -30,7 +34,7 @@ public void defaultProperties() @Test public void setProperties() { - Camera camera = new Camera(); + Camera camera = new Camera(mock(Screen.class)); camera.setClippingDistance(15f, 30f); assertEquals(15f, camera.getNearClip(), 0f); @@ -43,11 +47,22 @@ public void setProperties() @Test public void getProjectionMatrix() { - Camera camera = new Camera(); + Screen screen = mock(Screen.class); + when(screen.getAspect()).thenReturn(4f / 3f); + Camera camera = new Camera(screen); Matrix4f mat = new Matrix4f(); mat.perspective((float) Math.PI / 2f, 4f / 3f, 0.1f, 1000f); assertTrue(mat.equals(camera.getProjectionMatrix(), 0.0001f)); } + + @Test + public void externalTransform() + { + Transform3D transform = new Transform3D(); + Camera camera = new Camera(transform, mock(Screen.class)); + + assertTrue(transform == camera.getTransform()); + } } diff --git a/src/test/java/unit/CullGameObjectsActionTest.java b/src/test/java/unit/CullGameObjectsActionTest.java index bbfc68a..1a8b759 100644 --- a/src/test/java/unit/CullGameObjectsActionTest.java +++ b/src/test/java/unit/CullGameObjectsActionTest.java @@ -3,8 +3,9 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import org.junit.Test; -import net.whg.we.main.CullGameObjectsAction; +import net.whg.we.main.CullGameObjectsPipeline; import net.whg.we.main.GameObject; +import net.whg.we.main.PipelineConstants; import net.whg.we.main.Scene; public class CullGameObjectsActionTest @@ -22,7 +23,7 @@ public void runAction() scene.addGameObject(go3); go2.markForRemoval(); - CullGameObjectsAction action = new CullGameObjectsAction(); + CullGameObjectsPipeline action = new CullGameObjectsPipeline(); scene.addPipelineAction(action); action.run(); @@ -34,6 +35,6 @@ public void runAction() @Test public void defaultPriority() { - assertEquals(40000, new CullGameObjectsAction().getPriority()); + assertEquals(PipelineConstants.DISPOSE_GAMEOBJECTS, new CullGameObjectsPipeline().getPriority()); } } diff --git a/src/test/java/unit/FakeWindow.java b/src/test/java/unit/FakeWindow.java new file mode 100644 index 0000000..25107b9 --- /dev/null +++ b/src/test/java/unit/FakeWindow.java @@ -0,0 +1,60 @@ +package unit; + +import net.whg.we.rendering.IRenderingEngine; +import net.whg.we.window.IWindow; +import net.whg.we.window.IWindowListener; +import net.whg.we.window.WindowSettings; + +public class FakeWindow implements IWindow +{ + public IWindowListener listener; + public WindowSettings settings = new WindowSettings(); + + @Override + public void dispose() + {} + + @Override + public boolean isDisposed() + { + return false; + } + + @Override + public void setProperties(WindowSettings settings) + {} + + @Override + public WindowSettings getProperties() + { + return settings; + } + + @Override + public IRenderingEngine getRenderingEngine() + { + return null; + } + + @Override + public void addWindowListener(IWindowListener listener) + { + this.listener = listener; + } + + @Override + public void removeWindowListener(IWindowListener listener) + { + this.listener = null; + } + + @Override + public void pollEvents() + {} + + @Override + public long getWindowId() + { + return 1; + } +} diff --git a/src/test/java/unit/InputEndFrameActionTest.java b/src/test/java/unit/InputEndFrameActionTest.java new file mode 100644 index 0000000..88e0953 --- /dev/null +++ b/src/test/java/unit/InputEndFrameActionTest.java @@ -0,0 +1,29 @@ +package unit; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import org.junit.Test; +import net.whg.we.main.PipelineConstants; +import net.whg.we.window.Input; +import net.whg.we.window.InputEndFrameAction; + +public class InputEndFrameActionTest +{ + @Test + public void defaultPriority() + { + assertEquals(PipelineConstants.ENDFRAME, new InputEndFrameAction(mock(Input.class)).getPriority()); + } + + @Test + public void endFrame() + { + Input input = mock(Input.class); + InputEndFrameAction action = new InputEndFrameAction(input); + + action.run(); + + verify(input).endFrame(); + } +} diff --git a/src/test/java/unit/InputTest.java b/src/test/java/unit/InputTest.java index 2f712cf..a88be8e 100644 --- a/src/test/java/unit/InputTest.java +++ b/src/test/java/unit/InputTest.java @@ -2,198 +2,288 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; import org.junit.Test; -import net.whg.we.main.Input; -import net.whg.we.main.UserControlsUpdater; -import net.whg.we.window.IWindow; -import net.whg.we.window.IWindowListener; -import net.whg.we.window.WindowSettings; +import net.whg.we.window.Input; public class InputTest { - private IWindow window() - { - IWindow window = mock(IWindow.class); - when(window.getProperties()).thenReturn(new WindowSettings()); - - return window; - } - - private IWindowListener listener(IWindow window) - { - IWindowListener[] l = new IWindowListener[1]; - - doAnswer(a -> - { l[0] = a.getArgument(0); return null; }).when(window) - .addWindowListener(any()); - - UserControlsUpdater.bind(window); - return l[0]; - } - @Test public void keyPressed() { - Input.clear(); + FakeWindow window = new FakeWindow(); + Input input = new Input(window); - IWindow window = window(); - IWindowListener listener = listener(window); + window.listener.onKeyPressed(window, 35); - listener.onKeyPressed(window, 35); - - assertTrue(Input.isKeyDown(35)); - assertTrue(Input.isKeyJustDown(35)); - assertFalse(Input.isKeyJustUp(35)); + assertTrue(input.isKeyDown(35)); + assertTrue(input.isKeyJustDown(35)); + assertFalse(input.isKeyJustUp(35)); } @Test public void keyReleased() { - Input.clear(); - - IWindow window = window(); - IWindowListener listener = listener(window); + FakeWindow window = new FakeWindow(); + Input input = new Input(window); - listener.onKeyPressed(window, 35); - Input.endFrame(); - listener.onKeyReleased(window, 35); + window.listener.onKeyPressed(window, 35); + input.endFrame(); + window.listener.onKeyReleased(window, 35); - assertFalse(Input.isKeyDown(35)); - assertFalse(Input.isKeyJustDown(35)); - assertTrue(Input.isKeyJustUp(35)); + assertFalse(input.isKeyDown(35)); + assertFalse(input.isKeyJustDown(35)); + assertTrue(input.isKeyJustUp(35)); } @Test public void keyHeld() { - Input.clear(); - - IWindow window = window(); - IWindowListener listener = listener(window); + FakeWindow window = new FakeWindow(); + Input input = new Input(window); - listener.onKeyPressed(window, 35); - Input.endFrame(); + window.listener.onKeyPressed(window, 35); + input.endFrame(); - assertTrue(Input.isKeyDown(35)); - assertFalse(Input.isKeyJustDown(35)); - assertFalse(Input.isKeyJustUp(35)); + assertTrue(input.isKeyDown(35)); + assertFalse(input.isKeyJustDown(35)); + assertFalse(input.isKeyJustUp(35)); } @Test public void keyIdle() { - Input.clear(); - Input.endFrame(); + FakeWindow window = new FakeWindow(); + Input input = new Input(window); - assertFalse(Input.isKeyDown(35)); - assertFalse(Input.isKeyJustDown(35)); - assertFalse(Input.isKeyJustUp(35)); + input.endFrame(); + + assertFalse(input.isKeyDown(35)); + assertFalse(input.isKeyJustDown(35)); + assertFalse(input.isKeyJustUp(35)); } @Test public void mousePressed() { - Input.clear(); - - IWindow window = window(); - IWindowListener listener = listener(window); + FakeWindow window = new FakeWindow(); + Input input = new Input(window); - listener.onMousePressed(window, 2); + window.listener.onMousePressed(window, 2); - assertTrue(Input.isMouseButtonDown(2)); - assertTrue(Input.isMouseButtonJustDown(2)); - assertFalse(Input.isMouseButtonJustUp(2)); + assertTrue(input.isMouseButtonDown(2)); + assertTrue(input.isMouseButtonJustDown(2)); + assertFalse(input.isMouseButtonJustUp(2)); } @Test public void mouseReleased() { - Input.clear(); + FakeWindow window = new FakeWindow(); + Input input = new Input(window); - IWindow window = window(); - IWindowListener listener = listener(window); + window.listener.onMousePressed(window, 2); + input.endFrame(); + window.listener.onMouseReleased(window, 2); - listener.onMousePressed(window, 2); - Input.endFrame(); - listener.onMouseReleased(window, 2); - - assertFalse(Input.isMouseButtonDown(2)); - assertFalse(Input.isMouseButtonJustDown(2)); - assertTrue(Input.isMouseButtonJustUp(2)); + assertFalse(input.isMouseButtonDown(2)); + assertFalse(input.isMouseButtonJustDown(2)); + assertTrue(input.isMouseButtonJustUp(2)); } @Test public void mouseHeld() { - Input.clear(); - - IWindow window = window(); - IWindowListener listener = listener(window); + FakeWindow window = new FakeWindow(); + Input input = new Input(window); - listener.onMousePressed(window, 2); - Input.endFrame(); + window.listener.onMousePressed(window, 2); + input.endFrame(); - assertTrue(Input.isMouseButtonDown(2)); - assertFalse(Input.isMouseButtonJustDown(2)); - assertFalse(Input.isMouseButtonJustUp(2)); + assertTrue(input.isMouseButtonDown(2)); + assertFalse(input.isMouseButtonJustDown(2)); + assertFalse(input.isMouseButtonJustUp(2)); } @Test public void mouseIdle() { - Input.clear(); - Input.endFrame(); + FakeWindow window = new FakeWindow(); + Input input = new Input(window); - assertFalse(Input.isMouseButtonDown(1)); - assertFalse(Input.isMouseButtonJustDown(1)); - assertFalse(Input.isMouseButtonJustUp(1)); + input.endFrame(); + + assertFalse(input.isMouseButtonDown(1)); + assertFalse(input.isMouseButtonJustDown(1)); + assertFalse(input.isMouseButtonJustUp(1)); } @Test public void mouseMove() { - Input.clear(); - - IWindow window = window(); - IWindowListener listener = listener(window); + FakeWindow window = new FakeWindow(); + Input input = new Input(window); - listener.onMouseMove(window, 120f, 108f); + window.listener.onMouseMove(window, 120f, 108f); - assertEquals(120f, Input.getMouseX(), 0f); - assertEquals(108f, Input.getMouseY(), 0f); + assertEquals(120f, input.getMouseX(), 0f); + assertEquals(108f, input.getMouseY(), 0f); } @Test public void moveMouse_delta() { - Input.clear(); + FakeWindow window = new FakeWindow(); + Input input = new Input(window); - IWindow window = window(); - IWindowListener listener = listener(window); + window.listener.onMouseMove(window, 120f, 108f); + input.endFrame(); + window.listener.onMouseMove(window, 125f, 118f); - listener.onMouseMove(window, 120f, 108f); - Input.endFrame(); - listener.onMouseMove(window, 125f, 118f); - - assertEquals(5f, Input.getMouseDeltaX(), 0.0001f); - assertEquals(10f, Input.getMouseDeltaY(), 0.0001f); + assertEquals(5f, input.getMouseDeltaX(), 0.0001f); + assertEquals(10f, input.getMouseDeltaY(), 0.0001f); } @Test public void scrollWheel() { - Input.clear(); + FakeWindow window = new FakeWindow(); + Input input = new Input(window); + + window.listener.onMouseWheel(window, 0f, 3.5f); + + assertEquals(3.5f, input.getScrollWheelDelta(), 0f); + } + + @Test(expected = IllegalStateException.class) + public void endFrame_alreadyDisposed() + { + FakeWindow window = new FakeWindow(); + Input input = new Input(window); + input.dispose(); + + input.endFrame(); + } + + @Test(expected = IllegalStateException.class) + public void isKeyDown_alreadyDisposed() + { + FakeWindow window = new FakeWindow(); + Input input = new Input(window); + input.dispose(); - IWindow window = window(); - IWindowListener listener = listener(window); + input.isKeyDown(0); + } - listener.onMouseWheel(window, 0f, 3.5f); + @Test(expected = IllegalStateException.class) + public void isKeyJustDown_alreadyDisposed() + { + FakeWindow window = new FakeWindow(); + Input input = new Input(window); + input.dispose(); + + input.isKeyJustDown(0); + } + + @Test(expected = IllegalStateException.class) + public void isKeyJustUp_alreadyDisposed() + { + FakeWindow window = new FakeWindow(); + Input input = new Input(window); + input.dispose(); + + input.isKeyJustUp(0); + } + + @Test(expected = IllegalStateException.class) + public void getMouseX_alreadyDisposed() + { + FakeWindow window = new FakeWindow(); + Input input = new Input(window); + input.dispose(); + + input.getMouseX(); + } + + @Test(expected = IllegalStateException.class) + public void getMouseY_alreadyDisposed() + { + FakeWindow window = new FakeWindow(); + Input input = new Input(window); + input.dispose(); + + input.getMouseY(); + } + + @Test(expected = IllegalStateException.class) + public void getMouseDeltaX_alreadyDisposed() + { + FakeWindow window = new FakeWindow(); + Input input = new Input(window); + input.dispose(); + + input.getMouseDeltaX(); + } + + @Test(expected = IllegalStateException.class) + public void getMouseDeltaY_alreadyDisposed() + { + FakeWindow window = new FakeWindow(); + Input input = new Input(window); + input.dispose(); + + input.getMouseDeltaY(); + } + + @Test(expected = IllegalStateException.class) + public void isMouseButtonDown_alreadyDisposed() + { + FakeWindow window = new FakeWindow(); + Input input = new Input(window); + input.dispose(); + + input.isMouseButtonDown(0); + } + + @Test(expected = IllegalStateException.class) + public void isMouseButtonJustDown_alreadyDisposed() + { + FakeWindow window = new FakeWindow(); + Input input = new Input(window); + input.dispose(); + + input.isMouseButtonJustDown(0); + } + + @Test(expected = IllegalStateException.class) + public void isMouseButtonJustUp_alreadyDisposed() + { + FakeWindow window = new FakeWindow(); + Input input = new Input(window); + input.dispose(); + + input.isMouseButtonJustUp(0); + } + + @Test(expected = IllegalStateException.class) + public void getScrollWheelDelta_alreadyDisposed() + { + FakeWindow window = new FakeWindow(); + Input input = new Input(window); + input.dispose(); + + input.getScrollWheelDelta(); + } + + @Test + public void dispose() + { + FakeWindow window = new FakeWindow(); + Input input = new Input(window); + input.dispose(); + input.dispose(); - assertEquals(3.5f, Input.getScrollWheelDelta(), 0f); + assertNull(window.listener); } } diff --git a/src/test/java/unit/MaterialTest.java b/src/test/java/unit/MaterialTest.java index 43b1f4f..d975f49 100644 --- a/src/test/java/unit/MaterialTest.java +++ b/src/test/java/unit/MaterialTest.java @@ -10,6 +10,7 @@ import org.joml.Matrix4f; import org.junit.Test; import net.whg.we.rendering.Material; +import net.whg.we.window.Screen; import net.whg.we.rendering.Camera; import net.whg.we.rendering.IShader; import net.whg.we.rendering.ITexture; @@ -57,7 +58,7 @@ public void cameraMatrix() IShader shader = mock(IShader.class); Material material = new Material(shader); - Camera camera = new Camera(); + Camera camera = new Camera(mock(Screen.class)); Matrix4f matrix = new Matrix4f(); material.setCameraMatrix(camera, matrix); diff --git a/src/test/java/unit/PhysicsPipelineActionTest.java b/src/test/java/unit/PhysicsPipelineActionTest.java index a732d96..91d8475 100644 --- a/src/test/java/unit/PhysicsPipelineActionTest.java +++ b/src/test/java/unit/PhysicsPipelineActionTest.java @@ -2,23 +2,31 @@ import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import org.junit.Test; import net.whg.we.main.AbstractBehavior; import net.whg.we.main.IFixedUpdateable; -import net.whg.we.main.PhysicsPipelineAction; +import net.whg.we.main.PhysicsPipeline; +import net.whg.we.main.PipelineConstants; +import net.whg.we.main.Timer; public class PhysicsPipelineActionTest { @Test public void ensurePipelinePriority() { - assertEquals(-10000, new PhysicsPipelineAction().getPriority()); + assertEquals(PipelineConstants.PHYSICS_UPDATES, new PhysicsPipeline(mock(Timer.class)).getPriority()); } @Test public void updateBehaviors() { - PhysicsPipelineAction action = new PhysicsPipelineAction(); + Timer timer = mock(Timer.class); + when(timer.getIdealPhysicsFrame()).thenReturn(1L); + when(timer.getPhysicsFrame()).thenReturn(0L) + .thenReturn(1L); + + PhysicsPipeline action = new PhysicsPipeline(timer); action.enableBehavior(mock(AbstractBehavior.class)); // To make sure no casting issues occur UpdatableAction behavior = new UpdatableAction(); @@ -33,6 +41,39 @@ public void updateBehaviors() assertEquals(1, behavior.calls); } + @Test + public void update_twice() + { + Timer timer = mock(Timer.class); + when(timer.getIdealPhysicsFrame()).thenReturn(1L) + .thenReturn(2L); + when(timer.getPhysicsFrame()).thenReturn(0L) + .thenReturn(1L) + .thenReturn(2L); + + PhysicsPipeline action = new PhysicsPipeline(timer); + UpdatableAction behavior = new UpdatableAction(); + action.enableBehavior(behavior); + + action.run(); + assertEquals(2, behavior.calls); + } + + @Test + public void update_never() + { + Timer timer = mock(Timer.class); + when(timer.getIdealPhysicsFrame()).thenReturn(2L); + when(timer.getPhysicsFrame()).thenReturn(2L); + + PhysicsPipeline action = new PhysicsPipeline(timer); + UpdatableAction behavior = new UpdatableAction(); + action.enableBehavior(behavior); + + action.run(); + assertEquals(0, behavior.calls); + } + class UpdatableAction extends AbstractBehavior implements IFixedUpdateable { int calls = 0; diff --git a/src/test/java/unit/PollEventsActionTest.java b/src/test/java/unit/PollEventsActionTest.java new file mode 100644 index 0000000..5053875 --- /dev/null +++ b/src/test/java/unit/PollEventsActionTest.java @@ -0,0 +1,29 @@ +package unit; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import org.junit.Test; +import net.whg.we.main.PipelineConstants; +import net.whg.we.main.PollEventsPipeline; +import net.whg.we.window.IWindow; + +public class PollEventsActionTest +{ + @Test + public void defaultPriority() + { + assertEquals(PipelineConstants.POLL_WINDOW_EVENTS, new PollEventsPipeline(mock(IWindow.class)).getPriority()); + } + + @Test + public void pollEvents() + { + IWindow window = mock(IWindow.class); + PollEventsPipeline action = new PollEventsPipeline(window); + + action.run(); + + verify(window).pollEvents(); + } +} diff --git a/src/test/java/unit/RenderBehaviorTest.java b/src/test/java/unit/RenderBehaviorTest.java index 96524a7..cb89bdd 100644 --- a/src/test/java/unit/RenderBehaviorTest.java +++ b/src/test/java/unit/RenderBehaviorTest.java @@ -9,8 +9,9 @@ import org.junit.Test; import net.whg.we.main.GameObject; import net.whg.we.rendering.Material; -import net.whg.we.main.RenderBehavior; -import net.whg.we.main.RenderPipelineAction; +import net.whg.we.rendering.RenderBehavior; +import net.whg.we.rendering.RenderPipeline; +import net.whg.we.window.Screen; import net.whg.we.main.Scene; import net.whg.we.rendering.Camera; import net.whg.we.rendering.IMesh; @@ -75,8 +76,8 @@ public void render_normalConditions() go.addBehavior(behavior); scene.addGameObject(go); - RenderPipelineAction renderPipeline = new RenderPipelineAction(); - renderPipeline.setCamera(new Camera()); + RenderPipeline renderPipeline = new RenderPipeline(); + renderPipeline.setCamera(new Camera(mock(Screen.class))); renderPipeline.enableBehavior(behavior); renderPipeline.run(); @@ -99,8 +100,8 @@ public void render_behaviorAddedLater() scene.addGameObject(go); go.addBehavior(behavior); - RenderPipelineAction renderPipeline = new RenderPipelineAction(); - renderPipeline.setCamera(new Camera()); + RenderPipeline renderPipeline = new RenderPipeline(); + renderPipeline.setCamera(new Camera(mock(Screen.class))); renderPipeline.enableBehavior(behavior); renderPipeline.run(); @@ -121,8 +122,8 @@ public void render_noMesh_dontRender() scene.addGameObject(go); go.addBehavior(behavior); - RenderPipelineAction renderPipeline = new RenderPipelineAction(); - renderPipeline.setCamera(new Camera()); + RenderPipeline renderPipeline = new RenderPipeline(); + renderPipeline.setCamera(new Camera(mock(Screen.class))); renderPipeline.enableBehavior(behavior); renderPipeline.run(); @@ -141,8 +142,8 @@ public void render_noMaterial_dontRender() scene.addGameObject(go); go.addBehavior(behavior); - RenderPipelineAction renderPipeline = new RenderPipelineAction(); - renderPipeline.setCamera(new Camera()); + RenderPipeline renderPipeline = new RenderPipeline(); + renderPipeline.setCamera(new Camera(mock(Screen.class))); renderPipeline.enableBehavior(behavior); renderPipeline.run(); diff --git a/src/test/java/unit/RenderPipelineActionTest.java b/src/test/java/unit/RenderPipelineActionTest.java index ff85645..049b925 100644 --- a/src/test/java/unit/RenderPipelineActionTest.java +++ b/src/test/java/unit/RenderPipelineActionTest.java @@ -7,8 +7,10 @@ import static org.mockito.Mockito.verify; import org.junit.Test; import net.whg.we.main.GameObject; -import net.whg.we.main.RenderBehavior; -import net.whg.we.main.RenderPipelineAction; +import net.whg.we.main.PipelineConstants; +import net.whg.we.rendering.RenderBehavior; +import net.whg.we.rendering.RenderPipeline; +import net.whg.we.window.Screen; import net.whg.we.rendering.Camera; import net.whg.we.rendering.IMesh; import net.whg.we.rendering.Material; @@ -19,7 +21,7 @@ public class RenderPipelineActionTest public void addBehavior() { RenderBehavior behavior = new RenderBehavior(); - RenderPipelineAction action = new RenderPipelineAction(); + RenderPipeline action = new RenderPipeline(); action.enableBehavior(behavior); assertEquals(1, action.renderBehaviors() @@ -41,8 +43,8 @@ public void renderElements() behavior.setMaterial(material); go.addBehavior(behavior); - RenderPipelineAction action = new RenderPipelineAction(); - action.setCamera(new Camera()); + RenderPipeline action = new RenderPipeline(); + action.setCamera(new Camera(mock(Screen.class))); action.enableBehavior(behavior); action.run(); @@ -63,7 +65,7 @@ public void renderElements_noCamera() behavior.setMaterial(material); go.addBehavior(behavior); - RenderPipelineAction action = new RenderPipelineAction(); + RenderPipeline action = new RenderPipeline(); action.enableBehavior(behavior); action.run(); @@ -74,6 +76,15 @@ public void renderElements_noCamera() @Test public void ensureCorrectPriority() { - assertEquals(30000, new RenderPipelineAction().getPriority()); + assertEquals(PipelineConstants.RENDER_SOLIDS, new RenderPipeline().getPriority()); + } + + @Test + public void initializeWithCamera() + { + Camera camera = mock(Camera.class); + RenderPipeline pipeline = new RenderPipeline(camera); + + assertTrue(camera == pipeline.getCamera()); } } diff --git a/src/test/java/unit/ScreenClearPipelineTest.java b/src/test/java/unit/ScreenClearPipelineTest.java new file mode 100644 index 0000000..203ff85 --- /dev/null +++ b/src/test/java/unit/ScreenClearPipelineTest.java @@ -0,0 +1,30 @@ +package unit; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import org.junit.Test; +import net.whg.we.main.PipelineConstants; +import net.whg.we.rendering.IScreenClearHandler; +import net.whg.we.rendering.ScreenClearPipeline; + +public class ScreenClearPipelineTest +{ + @Test + public void defaultPriority() + { + assertEquals(PipelineConstants.CLEAR_SCREEN, + new ScreenClearPipeline(mock(IScreenClearHandler.class)).getPriority()); + } + + @Test + public void clearScreen() + { + IScreenClearHandler screenClear = mock(IScreenClearHandler.class); + ScreenClearPipeline pipeline = new ScreenClearPipeline(screenClear); + + pipeline.run(); + + verify(screenClear).clearScreen(); + } +} diff --git a/src/test/java/unit/ScreenTest.java b/src/test/java/unit/ScreenTest.java index ce8d2f5..a98dd8f 100644 --- a/src/test/java/unit/ScreenTest.java +++ b/src/test/java/unit/ScreenTest.java @@ -1,61 +1,90 @@ package unit; import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.junit.Assert.assertNull; import org.junit.Test; -import net.whg.we.main.Screen; -import net.whg.we.main.UserControlsUpdater; -import net.whg.we.window.IWindow; -import net.whg.we.window.IWindowListener; -import net.whg.we.window.WindowSettings; +import net.whg.we.window.Screen; public class ScreenTest { - private IWindowListener listener(IWindow window) + @Test + public void resizeScreen() { - IWindowListener[] l = new IWindowListener[1]; + FakeWindow window = new FakeWindow(); + Screen screen = new Screen(window); - doAnswer(a -> - { l[0] = a.getArgument(0); return null; }).when(window) - .addWindowListener(any()); + window.listener.onWindowResized(window, 320, 240); - UserControlsUpdater.bind(window); - return l[0]; + assertEquals(320, screen.getWidth()); + assertEquals(240, screen.getHeight()); + assertEquals(4f / 3f, screen.getAspect(), 0.0001f); } @Test - public void resizeScreen() + public void resizeScreen_updateTrigger() { - IWindow window = mock(IWindow.class); - when(window.getProperties()).thenReturn(new WindowSettings()); + FakeWindow window = new FakeWindow(); + window.settings.setSize(1600, 900); + + Screen screen = new Screen(window); + window.listener.onWindowUpdated(window); - IWindowListener listener = listener(window); + assertEquals(1600, screen.getWidth()); + assertEquals(900, screen.getHeight()); + assertEquals(16f / 9f, screen.getAspect(), 0.0001f); + } - listener.onWindowResized(window, 320, 240); + @Test(expected = IllegalStateException.class) + public void getWidth_alreadyDisposed() + { + FakeWindow window = new FakeWindow(); + Screen screen = new Screen(window); + screen.dispose(); + + screen.getWidth(); + } + + @Test(expected = IllegalStateException.class) + public void getHeight_alreadyDisposed() + { + FakeWindow window = new FakeWindow(); + Screen screen = new Screen(window); + screen.dispose(); - assertEquals(320, Screen.getWidth()); - assertEquals(240, Screen.getHeight()); - assertEquals(4f / 3f, Screen.getAspect(), 0.0001f); + screen.getHeight(); + } + + @Test(expected = IllegalStateException.class) + public void getAspect_alreadyDisposed() + { + FakeWindow window = new FakeWindow(); + Screen screen = new Screen(window); + screen.dispose(); + + screen.getAspect(); } @Test - public void resizeScreen_updateTrigger() + public void dipose() { - IWindow window = mock(IWindow.class); - WindowSettings settings = new WindowSettings(); - settings.setWidth(1600); - settings.setHeight(900); - when(window.getProperties()).thenReturn(settings); + FakeWindow window = new FakeWindow(); - IWindowListener listener = listener(window); + Screen screen = new Screen(window); + screen.dispose(); + screen.dispose(); // To make sure nothing happens when you dispose twice + + assertNull(window.listener); + } + + @Test + public void initializeOnCreation() + { + FakeWindow window = new FakeWindow(); + window.settings.setSize(400, 300); - listener.onWindowUpdated(window); + Screen screen = new Screen(window); - assertEquals(1600, Screen.getWidth()); - assertEquals(900, Screen.getHeight()); - assertEquals(16f / 9f, Screen.getAspect(), 0.0001f); + assertEquals(400, screen.getWidth()); + assertEquals(300, screen.getHeight()); } } diff --git a/src/test/java/unit/TimerActionTest.java b/src/test/java/unit/TimerActionTest.java index 44ec334..5f5cceb 100644 --- a/src/test/java/unit/TimerActionTest.java +++ b/src/test/java/unit/TimerActionTest.java @@ -4,6 +4,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import org.junit.Test; +import net.whg.we.main.PipelineConstants; import net.whg.we.main.Timer; import net.whg.we.main.TimerAction; @@ -23,6 +24,6 @@ public void beginFrameOnRun() @Test public void priorityIs_Negative1000000() { - assertEquals(-1000000, new TimerAction(mock(Timer.class)).getPriority()); + assertEquals(PipelineConstants.CALCULATE_TIMESTAMPS, new TimerAction(mock(Timer.class)).getPriority()); } } diff --git a/src/test/java/unit/UpdatePipelineActionTest.java b/src/test/java/unit/UpdatePipelineActionTest.java index ccd8aa0..e6c84ff 100644 --- a/src/test/java/unit/UpdatePipelineActionTest.java +++ b/src/test/java/unit/UpdatePipelineActionTest.java @@ -1,24 +1,28 @@ package unit; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; import org.junit.Test; import net.whg.we.main.AbstractBehavior; import net.whg.we.main.IUpdateable; -import net.whg.we.main.UpdatePipelineAction; +import net.whg.we.main.PipelineConstants; +import net.whg.we.main.Timer; +import net.whg.we.main.UpdatePipeline; public class UpdatePipelineActionTest { @Test public void ensurePipelinePriority() { - assertEquals(0, new UpdatePipelineAction().getPriority()); + assertEquals(PipelineConstants.FRAME_UPDATES, new UpdatePipeline(mock(Timer.class)).getPriority()); } @Test public void updateBehaviors() { - UpdatePipelineAction action = new UpdatePipelineAction(); + Timer timer = mock(Timer.class); + UpdatePipeline action = new UpdatePipeline(timer); action.enableBehavior(mock(AbstractBehavior.class)); // To make sure no casting issues occur UpdatableAction behavior = new UpdatableAction(); @@ -27,6 +31,7 @@ public void updateBehaviors() action.enableBehavior(behavior); action.run(); assertEquals(1, behavior.calls); + assertTrue(timer == behavior.timer); action.disableBehavior(behavior); action.run(); @@ -35,11 +40,13 @@ public void updateBehaviors() class UpdatableAction extends AbstractBehavior implements IUpdateable { + Timer timer; int calls = 0; @Override - public void update() + public void update(Timer timer) { + this.timer = timer; calls++; } } diff --git a/src/test/java/unit/UserControlsUpdaterTest.java b/src/test/java/unit/UserControlsUpdaterTest.java deleted file mode 100644 index 09b194c..0000000 --- a/src/test/java/unit/UserControlsUpdaterTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package unit; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import org.junit.Test; -import net.whg.we.main.UserControlsUpdater; -import net.whg.we.window.IWindow; -import net.whg.we.window.IWindowListener; -import net.whg.we.window.WindowSettings; - -public class UserControlsUpdaterTest -{ - private IWindowListener listener(IWindow window) - { - IWindowListener[] l = new IWindowListener[1]; - - doAnswer(a -> - { l[0] = a.getArgument(0); return null; }).when(window) - .addWindowListener(any()); - - UserControlsUpdater.bind(window); - return l[0]; - } - - @Test - public void unbindWindow() - { - IWindow window = mock(IWindow.class); - when(window.getProperties()).thenReturn(new WindowSettings()); - IWindowListener listener = listener(window); - - listener.onWindowRequestClose(window); - listener.onWindowDestroyed(window); - - assertNull(UserControlsUpdater.getBoundWindow()); - } - - @Test - public void bindWindow_twice() - { - IWindow window = mock(IWindow.class); - when(window.getProperties()).thenReturn(new WindowSettings()); - - UserControlsUpdater.bind(window); - UserControlsUpdater.bind(window); - - assertEquals(window, UserControlsUpdater.getBoundWindow()); - verify(window, never()).removeWindowListener(any()); - } -} diff --git a/src/test/java/unit/WindowCloseHandlerTest.java b/src/test/java/unit/WindowCloseHandlerTest.java new file mode 100644 index 0000000..c0c5787 --- /dev/null +++ b/src/test/java/unit/WindowCloseHandlerTest.java @@ -0,0 +1,58 @@ +package unit; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doAnswer; +import org.junit.Test; +import net.whg.we.main.GameLoop; +import net.whg.we.window.IWindow; +import net.whg.we.window.IWindowListener; +import net.whg.we.window.WindowCloseHandler; + +public class WindowCloseHandlerTest +{ + @Test + public void bindToWindow() + { + IWindow window = mock(IWindow.class); + GameLoop loop = mock(GameLoop.class); + + WindowCloseHandler.bindToWindow(window, loop); + + verify(window).addWindowListener(any()); + } + + @Test + public void dispose() + { + IWindow window = mock(IWindow.class); + GameLoop loop = mock(GameLoop.class); + + WindowCloseHandler handler = WindowCloseHandler.bindToWindow(window, loop); + + handler.dispose(); + handler.dispose(); + + verify(window, times(1)).removeWindowListener(any()); + } + + @Test + public void onClose() + { + IWindow window = mock(IWindow.class); + GameLoop loop = mock(GameLoop.class); + + IWindowListener[] l = new IWindowListener[1]; + doAnswer(a -> + { l[0] = a.getArgument(0); return null; }).when(window) + .addWindowListener(any()); + + WindowCloseHandler.bindToWindow(window, loop); + + l[0].onWindowRequestClose(window); + + verify(loop).stop(); + } +} diff --git a/src/test/res/cube.mtl b/src/test/res/cube.mtl deleted file mode 100644 index e8374a5..0000000 --- a/src/test/res/cube.mtl +++ /dev/null @@ -1,12 +0,0 @@ -# Blender MTL File: 'None' -# Material Count: 1 - -newmtl Material -Ns 323.999994 -Ka 1.000000 1.000000 1.000000 -Kd 0.800000 0.800000 0.800000 -Ks 0.500000 0.500000 0.500000 -Ke 0.000000 0.000000 0.000000 -Ni 1.450000 -d 1.000000 -illum 2 diff --git a/src/test/res/diffuse.frag b/src/test/res/diffuse.frag new file mode 100644 index 0000000..104dc2a --- /dev/null +++ b/src/test/res/diffuse.frag @@ -0,0 +1,17 @@ +#version 330 core + +const vec3 lightDir = vec3(0.12039, 0.963, 0.24077); + +uniform sampler2D diffuse; + +in vec3 pass_normal; +in vec2 pass_uv; + +out vec4 color; + +void main() +{ + vec3 col = texture(diffuse, pass_uv).rgb; + col *= dot(pass_normal, lightDir) * 0.15 + 0.85; + color = vec4(col, 1.0); +} \ No newline at end of file diff --git a/src/test/res/normal_shader.vert b/src/test/res/diffuse.vert similarity index 100% rename from src/test/res/normal_shader.vert rename to src/test/res/diffuse.vert diff --git a/src/test/res/normal_shader.frag b/src/test/res/normal_shader.frag deleted file mode 100644 index cd1c056..0000000 --- a/src/test/res/normal_shader.frag +++ /dev/null @@ -1,13 +0,0 @@ -#version 330 core - -uniform sampler2D diffuse; - -in vec3 pass_normal; -in vec2 pass_uv; - -out vec4 color; - -void main() -{ - color = vec4(texture(diffuse, pass_uv).rgb, 1.0); -} \ No newline at end of file