From 7f901a5240d057ab0f18bd46947d536dbfc453b7 Mon Sep 17 00:00:00 2001 From: math0898 <25396616+math0898@users.noreply.github.com> Date: Mon, 16 Jan 2023 12:35:24 -0600 Subject: [PATCH 1/8] Refactored GameEngine to be a Singleton --- build.gradle | 2 +- src/main/java/suga/engine/GameEngine.java | 51 +++++++++++++++---- src/main/java/suga/engine/game/BasicGame.java | 6 +-- .../suga/engine/graphics/GraphicsPanel.java | 2 +- .../engine/input/keyboard/BasicKeyMapper.java | 9 ++-- .../input/mouse/BasicMouseListener.java | 13 +++-- .../suga/engine/sound/JavaxSoundManager.java | 45 ++++++++-------- .../suga/engine/threads/GameLogicThread.java | 4 +- .../suga/engine/threads/GraphicsThread.java | 4 +- 9 files changed, 81 insertions(+), 55 deletions(-) diff --git a/build.gradle b/build.gradle index c6ad2c8..d3084e0 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ plugins { } group 'io.github.math0898' -version '2.5.1' +version '3.0.0-zeta1' repositories { mavenCentral() diff --git a/src/main/java/suga/engine/GameEngine.java b/src/main/java/suga/engine/GameEngine.java index 887367a..98aac92 100644 --- a/src/main/java/suga/engine/GameEngine.java +++ b/src/main/java/suga/engine/GameEngine.java @@ -21,25 +21,30 @@ */ public class GameEngine { + /** + * Reference to the single GameEngine that exists at runtime. + */ + private static GameEngine globalEngine; + /** * The currently opened frame. */ - protected static JFrame frame; + private JFrame frame; /** * The current graphics thread if one is running. */ - protected static SugaThread graphics; + private SugaThread graphics; /** * The current game logic thread if one is running. */ - protected static SugaThread logic; + private SugaThread logic; /** * The logger currently being used by the GameEngine. */ - protected static Logger logger = new GeneralLogger(); + private Logger logger = new GeneralLogger(); /** * An enum of pre-defined resolutions to open a game window at. @@ -54,12 +59,36 @@ public enum Window { FULL_SCREEN } + /** + * The GameEngine constructor. + */ + protected GameEngine () { + + } + + /** + * Creates a new GameEngine object and assigns it to the static reference. + */ + private static void createGameEngine () { + globalEngine = new GameEngine(); + } + + /** + * Accessor method to the global instance of the GameEngine. + * + * @return A reference to the GameEngine. + */ + public static GameEngine getInstance () { + if (globalEngine == null) createGameEngine(); + return globalEngine; + } + /** * Accessor method for the logger being used by the GameEngine. * * @return The logger currently in use by the GameEngine. */ - public static Logger getLogger () { + public Logger getLogger () { return logger; } @@ -68,16 +97,16 @@ public static Logger getLogger () { * * @param logger The new logger for the GameEngine to use. */ - public static void setLogger (Logger logger) { - GameEngine.logger.log("GameEngine: Switching to a new logger."); - GameEngine.logger = logger; - GameEngine.logger.log("GameEngine: Switched logger."); + public void setLogger (Logger logger) { + this.logger.log("GameEngine: Switching to a new logger."); + this.logger = logger; + this.logger.log("GameEngine: Switched logger."); } /** * Closes both the logic and graphics thread. */ - public static void stop () { + public void stop () { logger.log("GameEngine: Stopping the game."); graphics.setStopped(true); logic.setStopped(true); @@ -101,7 +130,7 @@ public static void stop () { * @param mouseListener The mouse listener to use for this window. Will override active frame. * @param game The game to attach to this window. Will override currently active panel or input listeners. */ - public static void launchGameWindow (int width, int height, String name, boolean border, GraphicsPanel panel, + public void launchGameWindow (int width, int height, String name, boolean border, GraphicsPanel panel, Color background, int logicRate, int frameRate, GameKeyListener keyListener, GameMouseListener mouseListener, Game game) { logger.log("GameEngine: Starting the game window."); diff --git a/src/main/java/suga/engine/game/BasicGame.java b/src/main/java/suga/engine/game/BasicGame.java index 06fa09e..ac08fce 100644 --- a/src/main/java/suga/engine/game/BasicGame.java +++ b/src/main/java/suga/engine/game/BasicGame.java @@ -159,7 +159,7 @@ public void loop () { @Override public void processInput () { if (loadedScene == null) { - GameEngine.getLogger().log("BasicGame: No loaded scene. Cannot process inputs.", Level.WARNING); + GameEngine.getInstance().getLogger().log("BasicGame: No loaded scene. Cannot process inputs.", Level.WARNING); return; } Stack mice = mouseListener.getEvents(); @@ -231,8 +231,8 @@ public void clear () { agents = new ArrayList<>(); objects = new HashMap<>(); if (panel != null) panel.clearListeners(); - else GameEngine.getLogger().log("A clear of game objects was requested but this game does not have an active panel.", Level.WARNING); - GameEngine.getLogger().log("Cleared game objects.", Level.INFO); + else GameEngine.getInstance().getLogger().log("A clear of game objects was requested but this game does not have an active panel.", Level.WARNING); + GameEngine.getInstance().getLogger().log("Cleared game objects.", Level.INFO); } /** diff --git a/src/main/java/suga/engine/graphics/GraphicsPanel.java b/src/main/java/suga/engine/graphics/GraphicsPanel.java index d3fa2c4..2bd752a 100644 --- a/src/main/java/suga/engine/graphics/GraphicsPanel.java +++ b/src/main/java/suga/engine/graphics/GraphicsPanel.java @@ -55,7 +55,7 @@ public void drawing (int width, int height) { l.applyChanges(width, height, this); } } catch (ConcurrentModificationException e) { - GameEngine.getLogger().log(e, Level.WARNING); // This sometimes occurs when loading while drawing a frame. + GameEngine.getInstance().getLogger().log(e, Level.WARNING); // This sometimes occurs when loading while drawing a frame. } } diff --git a/src/main/java/suga/engine/input/keyboard/BasicKeyMapper.java b/src/main/java/suga/engine/input/keyboard/BasicKeyMapper.java index a44c881..a534bf7 100644 --- a/src/main/java/suga/engine/input/keyboard/BasicKeyMapper.java +++ b/src/main/java/suga/engine/input/keyboard/BasicKeyMapper.java @@ -1,9 +1,8 @@ package suga.engine.input.keyboard; +import suga.engine.GameEngine; import suga.engine.logger.Level; -import static suga.engine.GameEngine.getLogger; - /** * The default KeyMapper works by creating an array the size of the maximally supported keycode value and storing the * resulting enum values at each keycode index. @@ -37,7 +36,7 @@ public BasicKeyMapper () { public KeyValue convert (int keycode) { KeyValue toReturn = values[keycode]; if (keycode != toReturn.getValue()) - getLogger().log("BasicKeyMapper: Converted: " + KeyValue.toEnum(keycode) + " => " + toReturn, Level.VERBOSE); + GameEngine.getInstance().getLogger().log("BasicKeyMapper: Converted: " + KeyValue.toEnum(keycode) + " => " + toReturn, Level.VERBOSE); return toReturn; } @@ -50,10 +49,10 @@ public KeyValue convert (int keycode) { @Override public void set (int keycode, KeyValue key) { if (keycode < 0 || keycode > 255) { - getLogger().log("BasicKeyMapper: Given keycode not supported [0,255]: " + keycode, Level.EXCEPTION); + GameEngine.getInstance().getLogger().log("BasicKeyMapper: Given keycode not supported [0,255]: " + keycode, Level.EXCEPTION); return; } - getLogger().log("BasicKeyMapper: Remapped key " + KeyValue.toEnum(keycode) + " to " + key + ".", Level.DEBUG); + GameEngine.getInstance().getLogger().log("BasicKeyMapper: Remapped key " + KeyValue.toEnum(keycode) + " to " + key + ".", Level.DEBUG); values[keycode] = key; } } diff --git a/src/main/java/suga/engine/input/mouse/BasicMouseListener.java b/src/main/java/suga/engine/input/mouse/BasicMouseListener.java index 93ad1f3..75b1e15 100644 --- a/src/main/java/suga/engine/input/mouse/BasicMouseListener.java +++ b/src/main/java/suga/engine/input/mouse/BasicMouseListener.java @@ -1,5 +1,6 @@ package suga.engine.input.mouse; +import suga.engine.GameEngine; import suga.engine.logger.Level; import javax.swing.*; @@ -7,8 +8,6 @@ import java.awt.event.MouseEvent; import java.util.Stack; -import static suga.engine.GameEngine.getLogger; - /** * The BasicMouseListener is a simple implementation of the GameMouseListener which only pays attention to mouse presses * and mouse releases. @@ -51,10 +50,10 @@ public BasicMouseListener (JFrame frame) { @Override public Point getMousePos () { if (frame == null) { - getLogger().log("BasicMouseListener: Attempted to read mouse position when JFrame is not initialized.", Level.EXCEPTION); + GameEngine.getInstance().getLogger().log("BasicMouseListener: Attempted to read mouse position when JFrame is not initialized.", Level.EXCEPTION); return null; } - getLogger().log("BasicMouseListener: Read mouse position: " + frame.getMousePosition(), Level.VERBOSE); + GameEngine.getInstance().getLogger().log("BasicMouseListener: Read mouse position: " + frame.getMousePosition(), Level.VERBOSE); return frame.getMousePosition(); } @@ -76,7 +75,7 @@ public void mouseClicked (MouseEvent e) { */ @Override public void mousePressed (MouseEvent e) { - getLogger().log("BasicMouseListener: Received mouse pressed event: " + e, Level.VERBOSE); + GameEngine.getInstance().getLogger().log("BasicMouseListener: Received mouse pressed event: " + e, Level.VERBOSE); events.add(e); } @@ -87,7 +86,7 @@ public void mousePressed (MouseEvent e) { */ @Override public void mouseReleased (MouseEvent e) { - getLogger().log("BasicMouseListener: Received mouse release event: " + e, Level.VERBOSE); + GameEngine.getInstance().getLogger().log("BasicMouseListener: Received mouse release event: " + e, Level.VERBOSE); events.add(e); } @@ -128,7 +127,7 @@ public Stack getEvents () { */ @Override public void setFrame (JFrame frame) { - getLogger().log("BasicMouseListener: Assigning this listener to a new JFrame.", Level.DEBUG); + GameEngine.getInstance().getLogger().log("BasicMouseListener: Assigning this listener to a new JFrame.", Level.DEBUG); frame.addMouseListener(this); this.frame = frame; } diff --git a/src/main/java/suga/engine/sound/JavaxSoundManager.java b/src/main/java/suga/engine/sound/JavaxSoundManager.java index 3c6a186..85fc750 100644 --- a/src/main/java/suga/engine/sound/JavaxSoundManager.java +++ b/src/main/java/suga/engine/sound/JavaxSoundManager.java @@ -1,5 +1,6 @@ package suga.engine.sound; +import suga.engine.GameEngine; import suga.engine.logger.Level; import javax.sound.sampled.*; @@ -8,8 +9,6 @@ import java.util.Map; import java.util.Random; -import static suga.engine.GameEngine.getLogger; - /** * The JavaxSoundManager is a SoundManager that outputs sound using the classes provided by javax and javax.sampled. * @@ -59,17 +58,17 @@ protected Clip findClip (String path) { stream = new BufferedInputStream(stream); toReturn.open(AudioSystem.getAudioInputStream(stream)); } catch (IOException | UnsupportedAudioFileException | LineUnavailableException e) { - getLogger().log("JavaxSoundManager: An exception occurred attempting to convert the input stream to a clip.", e, Level.EXCEPTION); + GameEngine.getInstance().getLogger().log("JavaxSoundManager: An exception occurred attempting to convert the input stream to a clip.", e, Level.EXCEPTION); return null; } } catch (IOException e) { - getLogger().log("JavaxSoundManager: Clip was not found as a resource, and an exception occurred searching the disk.", e, Level.EXCEPTION); + GameEngine.getInstance().getLogger().log("JavaxSoundManager: Clip was not found as a resource, and an exception occurred searching the disk.", e, Level.EXCEPTION); return null; } } return toReturn; } catch (LineUnavailableException e) { - getLogger().log("JavaxSoundManager: Out line is not available.", e, Level.EXCEPTION); + GameEngine.getInstance().getLogger().log("JavaxSoundManager: Out line is not available.", e, Level.EXCEPTION); return null; } } @@ -85,7 +84,7 @@ public void clearSounds () { musicTracks.clear(); ambienceSounds.clear(); ambienceIntervals.clear(); - getLogger().log("JavaxSoundManager: Cleared all loaded sounds.", Level.DEBUG); + GameEngine.getInstance().getLogger().log("JavaxSoundManager: Cleared all loaded sounds.", Level.DEBUG); } /** @@ -99,11 +98,11 @@ public void clearSounds () { public boolean addSoundEffect (String name, String path) { Clip c = findClip(path); if (c == null) { - getLogger().log("JavaxSoundManager: Was unable to add sound effect by the name of: " + name, Level.EXCEPTION); + GameEngine.getInstance().getLogger().log("JavaxSoundManager: Was unable to add sound effect by the name of: " + name, Level.EXCEPTION); return false; } soundEffects.put(name, c); - getLogger().log("JavaxSoundManager: Added sound effect by the name of: " + name, Level.DEBUG); + GameEngine.getInstance().getLogger().log("JavaxSoundManager: Added sound effect by the name of: " + name, Level.DEBUG); return true; } @@ -115,7 +114,7 @@ public boolean addSoundEffect (String name, String path) { @Override public void removeSoundEffect (String name) { soundEffects.remove(name).close(); - getLogger().log("JavaxSoundManager: Removed sound effect by the name of: " + name, Level.DEBUG); + GameEngine.getInstance().getLogger().log("JavaxSoundManager: Removed sound effect by the name of: " + name, Level.DEBUG); } /** @@ -143,9 +142,9 @@ public void playSoundEffect (String name, float vol) { FloatControl control = (FloatControl) effect.getControl(FloatControl.Type.VOLUME); assert control != null; // Checked if supported first. control.setValue(vol); - } else getLogger().log("JavaxSoundManager: Attempted to modify volume of clip but action is not supported.", Level.WARNING); - getLogger().log("JavaxSoundManager: Played requested sound effect: " + name, Level.DEBUG); - } else getLogger().log("JavaxSoundManager: Effect was not found in the map of sound effects: " + name, Level.EXCEPTION); + } else GameEngine.getInstance().getLogger().log("JavaxSoundManager: Attempted to modify volume of clip but action is not supported.", Level.WARNING); + GameEngine.getInstance().getLogger().log("JavaxSoundManager: Played requested sound effect: " + name, Level.DEBUG); + } else GameEngine.getInstance().getLogger().log("JavaxSoundManager: Effect was not found in the map of sound effects: " + name, Level.EXCEPTION); } /** @@ -159,11 +158,11 @@ public void playSoundEffect (String name, float vol) { public boolean addMusicTrack (String name, String path) { Clip c = findClip(path); if (c == null) { - getLogger().log("JavaxSoundManager: Was unable to add music track by the name of: " + name, Level.EXCEPTION); + GameEngine.getInstance().getLogger().log("JavaxSoundManager: Was unable to add music track by the name of: " + name, Level.EXCEPTION); return false; } musicTracks.put(name, c); - getLogger().log("JavaxSoundManager: Added music track by the name of: " + name, Level.DEBUG); + GameEngine.getInstance().getLogger().log("JavaxSoundManager: Added music track by the name of: " + name, Level.DEBUG); return true; } @@ -175,7 +174,7 @@ public boolean addMusicTrack (String name, String path) { @Override public void removeMusicTrack (String name) { musicTracks.remove(name).close(); - getLogger().log("JavaxSoundManager: Removed music track by the name of: " + name, Level.DEBUG); + GameEngine.getInstance().getLogger().log("JavaxSoundManager: Removed music track by the name of: " + name, Level.DEBUG); } /** @@ -208,9 +207,9 @@ public void playMusicTrack (String name, float vol) { FloatControl control = (FloatControl) track.getControl(FloatControl.Type.VOLUME); assert control != null; // Checked if supported first. control.setValue(vol); - } else getLogger().log("JavaxSoundManager: Attempted to modify volume of clip but action is not supported.", Level.WARNING); - getLogger().log("JavaxSoundManager: Played requested music track: " + name, Level.DEBUG); - } else getLogger().log("JavaxSoundManager: Track was not found in the map of music tracks: " + name, Level.EXCEPTION); + } else GameEngine.getInstance().getLogger().log("JavaxSoundManager: Attempted to modify volume of clip but action is not supported.", Level.WARNING); + GameEngine.getInstance().getLogger().log("JavaxSoundManager: Played requested music track: " + name, Level.DEBUG); + } else GameEngine.getInstance().getLogger().log("JavaxSoundManager: Track was not found in the map of music tracks: " + name, Level.EXCEPTION); } /** @@ -221,7 +220,7 @@ public void stopMusic () { if (nowPlaying != null) { nowPlaying.stop(); nowPlaying = null; - getLogger().log("JavaxSoundManager: Stopped currently playing music track.", Level.DEBUG); + GameEngine.getInstance().getLogger().log("JavaxSoundManager: Stopped currently playing music track.", Level.DEBUG); } } @@ -239,12 +238,12 @@ public void stopMusic () { public boolean addAmbienceEffect (int interval, String name, String path) { Clip c = findClip(path); if (c == null) { - getLogger().log("JavaxSoundManager: Was unable to add ambience effect by the name of: " + name, Level.EXCEPTION); + GameEngine.getInstance().getLogger().log("JavaxSoundManager: Was unable to add ambience effect by the name of: " + name, Level.EXCEPTION); return false; } ambienceSounds.put(name, c); ambienceIntervals.put(name, interval); - getLogger().log("JavaxSoundManager: Added ambience effect by the name of: " + name, Level.DEBUG); + GameEngine.getInstance().getLogger().log("JavaxSoundManager: Added ambience effect by the name of: " + name, Level.DEBUG); return true; } @@ -257,7 +256,7 @@ public boolean addAmbienceEffect (int interval, String name, String path) { public void removeAmbienceEffect (String name) { ambienceSounds.remove(name).close(); ambienceIntervals.remove(name); - getLogger().log("JavaxSoundManager: Removed ambience effect by the name of: " + name, Level.DEBUG); + GameEngine.getInstance().getLogger().log("JavaxSoundManager: Removed ambience effect by the name of: " + name, Level.DEBUG); } /** @@ -270,7 +269,7 @@ public void ambienceSoundRoll () { double chance = 1.0 / ambienceIntervals.get(name); if (rand.nextDouble() <= chance) { clip.start(); - getLogger().log("JavaxSoundManager: Played ambience effect: " + name, Level.VERBOSE); + GameEngine.getInstance().getLogger().log("JavaxSoundManager: Played ambience effect: " + name, Level.VERBOSE); } }); } diff --git a/src/main/java/suga/engine/threads/GameLogicThread.java b/src/main/java/suga/engine/threads/GameLogicThread.java index 5b0d089..fa8a5dd 100644 --- a/src/main/java/suga/engine/threads/GameLogicThread.java +++ b/src/main/java/suga/engine/threads/GameLogicThread.java @@ -91,7 +91,7 @@ public void run () { //noinspection BusyWait sleep((1000 / LOGIC_RATE) - logicTime); } catch (Exception e) { - GameEngine.getLogger().log(e); + GameEngine.getInstance().getLogger().log(e); } } lastFinished = System.currentTimeMillis(); @@ -100,7 +100,7 @@ public void run () { try { game.loop(); } catch (Exception e) { - GameEngine.getLogger().log(e); + GameEngine.getInstance().getLogger().log(e); } } } diff --git a/src/main/java/suga/engine/threads/GraphicsThread.java b/src/main/java/suga/engine/threads/GraphicsThread.java index 0c98f4e..9392164 100644 --- a/src/main/java/suga/engine/threads/GraphicsThread.java +++ b/src/main/java/suga/engine/threads/GraphicsThread.java @@ -102,7 +102,7 @@ public void run () { //noinspection BusyWait sleep((int) ((1000 / FRAME_RATE) - drawTime)); } catch (Exception e) { - GameEngine.getLogger().log(e); + GameEngine.getInstance().getLogger().log(e); } } lastFinished = System.currentTimeMillis(); @@ -110,7 +110,7 @@ public void run () { try { panel.repaint(); } catch (Exception e) { - GameEngine.getLogger().log(e); + GameEngine.getInstance().getLogger().log(e); } } frames++; From 988e06e36d9671ab646d367c7806cde45b535f66 Mon Sep 17 00:00:00 2001 From: math0898 <25396616+math0898@users.noreply.github.com> Date: Mon, 16 Jan 2023 12:43:45 -0600 Subject: [PATCH 2/8] Snapshot Package --- build.gradle | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index d3084e0..2ab6ee5 100644 --- a/build.gradle +++ b/build.gradle @@ -6,6 +6,7 @@ plugins { group 'io.github.math0898' version '3.0.0-zeta1' +var isSnapshot = true repositories { mavenCentral() @@ -48,7 +49,10 @@ publishing { publications { maven(MavenPublication) { groupId(group) - artifactId('suga-engine') + if (isSnapshot) + artifactId('suga-engine-snapshot') + else + artifactId('suga-engine') version(version) // Pulls version from Gradle root from components.java From 4203f8522db1e653b28fde38b1a2fe802edf13c1 Mon Sep 17 00:00:00 2001 From: math0898 <25396616+math0898@users.noreply.github.com> Date: Mon, 16 Jan 2023 15:30:10 -0600 Subject: [PATCH 3/8] New Window Options --- build.gradle | 2 +- src/main/java/suga/engine/GameEngine.java | 37 ++++++++++++++++++----- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index 2ab6ee5..aaa90e5 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ plugins { } group 'io.github.math0898' -version '3.0.0-zeta1' +version '3.0.0-zeta2' var isSnapshot = true repositories { diff --git a/src/main/java/suga/engine/GameEngine.java b/src/main/java/suga/engine/GameEngine.java index 98aac92..faa826b 100644 --- a/src/main/java/suga/engine/GameEngine.java +++ b/src/main/java/suga/engine/GameEngine.java @@ -56,7 +56,32 @@ public enum Window { /** * Creates the window at the largest possible resolution in full screen mode. */ - FULL_SCREEN + FULL_SCREEN, + + /** + * Creates the window at half the maximum size in each direction. Leaves top bar. + */ + WINDOWED; + + /** + * Modifies the given frame's size depending on the given Window parameter. + * + * @param frame The frame that should be modified to match the window state. + */ + public void modifyFrame (JFrame frame) { + Dimension dim = Toolkit.getDefaultToolkit().getScreenSize(); + switch (this) { + case FULL_SCREEN -> { + frame.setSize(dim); + frame.setExtendedState(JFrame.MAXIMIZED_BOTH); + frame.setUndecorated(true); + } + case WINDOWED -> { + frame.setSize(dim.width / 2, dim.height / 2); + frame.setUndecorated(false); + } + } + } } /** @@ -118,10 +143,8 @@ public void stop () { /** * Creates a new game window with all the possible configuration options being specified. * - * @param width The width to create the game window at. - * @param height The height to create the game window at. + * @param window An enum parameter which is used to create the desired window size. * @param name The name for the resulting window. - * @param border Whether to hide the border or not when creating the window. * @param panel The graphics panel to be used for this game. * @param background The background color for the panel. * @param logicRate How many times per second the game logic should be called. @@ -130,17 +153,15 @@ public void stop () { * @param mouseListener The mouse listener to use for this window. Will override active frame. * @param game The game to attach to this window. Will override currently active panel or input listeners. */ - public void launchGameWindow (int width, int height, String name, boolean border, GraphicsPanel panel, + public void launchGameWindow (Window window, String name, GraphicsPanel panel, Color background, int logicRate, int frameRate, GameKeyListener keyListener, GameMouseListener mouseListener, Game game) { logger.log("GameEngine: Starting the game window."); panel.setBackground(background); frame = new JFrame(name); - frame.setSize(width, height); + window.modifyFrame(frame); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.getContentPane().add(panel, BorderLayout.CENTER); - frame.setExtendedState(JFrame.MAXIMIZED_BOTH); - frame.setUndecorated(!border); frame.setVisible(true); graphics = new GraphicsThread(panel, frameRate); graphics.start(); From 0c64dce657ccf2aaed5109fa97499434fad8deb9 Mon Sep 17 00:00:00 2001 From: math0898 <25396616+math0898@users.noreply.github.com> Date: Mon, 16 Jan 2023 17:35:50 -0600 Subject: [PATCH 4/8] Centered Window --- src/main/java/suga/engine/GameEngine.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/suga/engine/GameEngine.java b/src/main/java/suga/engine/GameEngine.java index faa826b..74c718b 100644 --- a/src/main/java/suga/engine/GameEngine.java +++ b/src/main/java/suga/engine/GameEngine.java @@ -160,6 +160,8 @@ public void launchGameWindow (Window window, String name, GraphicsPanel panel, panel.setBackground(background); frame = new JFrame(name); window.modifyFrame(frame); + Dimension dim = Toolkit.getDefaultToolkit().getScreenSize(); + frame.setLocation(dim.width / 2, dim.height / 2); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.getContentPane().add(panel, BorderLayout.CENTER); frame.setVisible(true); From 5a4d707132a36868e79a64a056dbb3b85edd0535 Mon Sep 17 00:00:00 2001 From: math0898 <25396616+math0898@users.noreply.github.com> Date: Mon, 16 Jan 2023 17:36:55 -0600 Subject: [PATCH 5/8] Increment Version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index aaa90e5..8e685b2 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ plugins { } group 'io.github.math0898' -version '3.0.0-zeta2' +version '3.0.0-zeta3' var isSnapshot = true repositories { From 16f1976afe92d41beeccdd049208d423f55326f7 Mon Sep 17 00:00:00 2001 From: math0898 <25396616+math0898@users.noreply.github.com> Date: Mon, 16 Jan 2023 17:41:24 -0600 Subject: [PATCH 6/8] Centered Window --- build.gradle | 2 +- src/main/java/suga/engine/GameEngine.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 8e685b2..9569f81 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ plugins { } group 'io.github.math0898' -version '3.0.0-zeta3' +version '3.0.0-zeta4' var isSnapshot = true repositories { diff --git a/src/main/java/suga/engine/GameEngine.java b/src/main/java/suga/engine/GameEngine.java index 74c718b..c528305 100644 --- a/src/main/java/suga/engine/GameEngine.java +++ b/src/main/java/suga/engine/GameEngine.java @@ -161,7 +161,7 @@ public void launchGameWindow (Window window, String name, GraphicsPanel panel, frame = new JFrame(name); window.modifyFrame(frame); Dimension dim = Toolkit.getDefaultToolkit().getScreenSize(); - frame.setLocation(dim.width / 2, dim.height / 2); + frame.setLocation((dim.width / 2) + (dim.width / 4), (dim.height / 2) + (dim.height / 4)); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.getContentPane().add(panel, BorderLayout.CENTER); frame.setVisible(true); From 2639b9720396004988d98a27543f63ec437b161e Mon Sep 17 00:00:00 2001 From: math0898 <25396616+math0898@users.noreply.github.com> Date: Mon, 16 Jan 2023 17:44:00 -0600 Subject: [PATCH 7/8] Centered Window --- build.gradle | 2 +- src/main/java/suga/engine/GameEngine.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 9569f81..5249e1c 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ plugins { } group 'io.github.math0898' -version '3.0.0-zeta4' +version '3.0.0-zeta5' var isSnapshot = true repositories { diff --git a/src/main/java/suga/engine/GameEngine.java b/src/main/java/suga/engine/GameEngine.java index c528305..9fdbccd 100644 --- a/src/main/java/suga/engine/GameEngine.java +++ b/src/main/java/suga/engine/GameEngine.java @@ -161,7 +161,7 @@ public void launchGameWindow (Window window, String name, GraphicsPanel panel, frame = new JFrame(name); window.modifyFrame(frame); Dimension dim = Toolkit.getDefaultToolkit().getScreenSize(); - frame.setLocation((dim.width / 2) + (dim.width / 4), (dim.height / 2) + (dim.height / 4)); + frame.setLocation(dim.width / 4, dim.height / 4); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.getContentPane().add(panel, BorderLayout.CENTER); frame.setVisible(true); From 8734d848fae7fff608e9a6920477d90734932493 Mon Sep 17 00:00:00 2001 From: math0898 <25396616+math0898@users.noreply.github.com> Date: Mon, 26 May 2025 00:23:17 -0500 Subject: [PATCH 8/8] Thread Timing Improvements (#37) * Algorithm Created - Designed and partially tested an algorithm which more closely hits target frame rates. - Optimized imports. - Weakened GraphicsPanel object in GraphicsThread to GraphicsPanelInterface. - Added repaint() to GraphicsPanelInterface. This is typically implemented by Component. [API CHANGE!!!] - Created GraphicsThreadTest. - Started writing test for run(). * Optimizations - Slightly modified algorithm which did result in an 'overperformance' error of 1.5%. After removing some 'unused code' now at roughly 9%. - Created parameterized test for running at requested frame rate. - Updated Gradle wrapper to Gradle 7.5. * Mostly Done - Shortened test durations. - Made tests automatically pause with an easy way to re-enable them. - 'Normalized' the 'remainingMillis' count when determining how long to wait by subtracting the start time. - Added a simple report result message so tests can still be useful. * FrameRate - Added setters and getters for the current target frame rate. * - Extracted commonalities between SugaThread implementing classes to AbstractThread. * - Wrote tests for GameLogicThread. * - Attempted to fix workflow. * - Finished fixing merge differences. --- .github/workflows/build.yml | 18 +-- .github/workflows/publish.yml | 6 +- build.gradle | 6 +- gradle/wrapper/gradle-wrapper.properties | 2 +- src/main/java/suga/engine/GameEngine.java | 4 +- src/main/java/suga/engine/game/BasicGame.java | 4 +- .../java/suga/engine/game/BasicScene.java | 1 + src/main/java/suga/engine/game/Game.java | 2 +- .../engine/game/objects/BasicGameObject.java | 2 +- .../graphics/GraphicsPanelInterface.java | 6 + .../suga/engine/sound/JavaxSoundManager.java | 5 +- .../suga/engine/threads/AbstractThread.java | 115 ++++++++++++++++++ .../suga/engine/threads/GameLogicThread.java | 80 +++--------- .../suga/engine/threads/GraphicsThread.java | 104 +++------------- .../physics/hitboxes/SquareHitBoxTest.java | 2 +- .../engine/threads/GameLogicThreadTest.java | 59 +++++++++ .../engine/threads/GraphicsThreadTest.java | 59 +++++++++ .../resources/suga/engine/threads/run.csv | 9 ++ 18 files changed, 312 insertions(+), 172 deletions(-) create mode 100644 src/main/java/suga/engine/threads/AbstractThread.java create mode 100644 src/test/java/suga/engine/threads/GameLogicThreadTest.java create mode 100644 src/test/java/suga/engine/threads/GraphicsThreadTest.java create mode 100644 src/test/resources/suga/engine/threads/run.csv diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2fe6d90..b436430 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,12 +8,14 @@ env: jobs: build: runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '16' + java-version: '24' cache: gradle - name: Validate Gradle wrapper uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b @@ -22,12 +24,14 @@ jobs: - run: gradle assemble --no-daemon test: runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '16' + java-version: '24' cache: gradle - name: Validate Gradle wrapper uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b @@ -43,7 +47,7 @@ jobs: - uses: actions/setup-java@master with: distribution: 'temurin' - java-version: '16' + java-version: '24' cache: gradle - name: Validate Gradle wrapper uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 81a2419..36cf74d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,10 +13,10 @@ jobs: contents: read packages: write steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 with: - java-version: '16' + java-version: '24' distribution: 'temurin' - name: Validate Gradle wrapper uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b diff --git a/build.gradle b/build.gradle index 5249e1c..c9814ba 100644 --- a/build.gradle +++ b/build.gradle @@ -13,11 +13,11 @@ repositories { } compileJava { - options.release = 19 + options.release = 24 } jacoco { - toolVersion = "0.8.8" + toolVersion = "0.8.13" } jacocoTestReport { @@ -28,7 +28,7 @@ jacocoTestReport { java { toolchain { - languageVersion = JavaLanguageVersion.of(19) + languageVersion = JavaLanguageVersion.of(24) } withSourcesJar() withJavadocJar() diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ffed3a2..8049c68 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/main/java/suga/engine/GameEngine.java b/src/main/java/suga/engine/GameEngine.java index 9fdbccd..ccb2d3a 100644 --- a/src/main/java/suga/engine/GameEngine.java +++ b/src/main/java/suga/engine/GameEngine.java @@ -1,14 +1,14 @@ package suga.engine; +import suga.engine.game.Game; import suga.engine.graphics.GraphicsPanel; +import suga.engine.input.keyboard.GameKeyListener; import suga.engine.input.mouse.GameMouseListener; import suga.engine.logger.GeneralLogger; import suga.engine.logger.Logger; import suga.engine.threads.GameLogicThread; import suga.engine.threads.GraphicsThread; import suga.engine.threads.SugaThread; -import suga.engine.input.keyboard.GameKeyListener; -import suga.engine.game.Game; import javax.swing.*; import java.awt.*; diff --git a/src/main/java/suga/engine/game/BasicGame.java b/src/main/java/suga/engine/game/BasicGame.java index ac08fce..300c656 100644 --- a/src/main/java/suga/engine/game/BasicGame.java +++ b/src/main/java/suga/engine/game/BasicGame.java @@ -5,6 +5,8 @@ import suga.engine.game.objects.GameObject; import suga.engine.graphics.DrawListener; import suga.engine.graphics.GraphicsPanel; +import suga.engine.input.keyboard.GameKeyListener; +import suga.engine.input.keyboard.KeyValue; import suga.engine.input.mouse.BasicMouseListener; import suga.engine.input.mouse.GameMouseListener; import suga.engine.logger.Level; @@ -12,8 +14,6 @@ import suga.engine.physics.PhysicsEngine; import suga.engine.physics.collidables.Collidable; import suga.engine.threads.SugaThread; -import suga.engine.input.keyboard.GameKeyListener; -import suga.engine.input.keyboard.KeyValue; import java.awt.event.MouseEvent; import java.util.*; diff --git a/src/main/java/suga/engine/game/BasicScene.java b/src/main/java/suga/engine/game/BasicScene.java index 2b690ee..623dc91 100644 --- a/src/main/java/suga/engine/game/BasicScene.java +++ b/src/main/java/suga/engine/game/BasicScene.java @@ -1,6 +1,7 @@ package suga.engine.game; import suga.engine.input.keyboard.KeyValue; + import java.awt.*; /** diff --git a/src/main/java/suga/engine/game/Game.java b/src/main/java/suga/engine/game/Game.java index 889e5c9..c6a7b23 100644 --- a/src/main/java/suga/engine/game/Game.java +++ b/src/main/java/suga/engine/game/Game.java @@ -4,9 +4,9 @@ import suga.engine.game.objects.GameObject; import suga.engine.graphics.DrawListener; import suga.engine.graphics.GraphicsPanel; +import suga.engine.input.keyboard.GameKeyListener; import suga.engine.input.mouse.GameMouseListener; import suga.engine.threads.SugaThread; -import suga.engine.input.keyboard.GameKeyListener; /** * Games require a main game loop to run along with game components that need to be run every game cycle. diff --git a/src/main/java/suga/engine/game/objects/BasicGameObject.java b/src/main/java/suga/engine/game/objects/BasicGameObject.java index db0043b..f4d0897 100644 --- a/src/main/java/suga/engine/game/objects/BasicGameObject.java +++ b/src/main/java/suga/engine/game/objects/BasicGameObject.java @@ -1,7 +1,7 @@ package suga.engine.game.objects; -import suga.engine.graphics.GraphicsPanel; import suga.engine.graphics.DrawListener; +import suga.engine.graphics.GraphicsPanel; import suga.engine.physics.BasicPhysical; import suga.engine.physics.Vector; import suga.engine.physics.collidables.Collidable; diff --git a/src/main/java/suga/engine/graphics/GraphicsPanelInterface.java b/src/main/java/suga/engine/graphics/GraphicsPanelInterface.java index c70f59c..405cf09 100644 --- a/src/main/java/suga/engine/graphics/GraphicsPanelInterface.java +++ b/src/main/java/suga/engine/graphics/GraphicsPanelInterface.java @@ -99,4 +99,10 @@ public interface GraphicsPanelInterface { * @param image The image to draw to the screen. */ void addImage (int x, int y, int width, int height, BufferedImage image); + + /** + * Called each frame by the GraphicsThread. This method should be inherited from JComponent but is defined in + * Component. + */ + void repaint (); } diff --git a/src/main/java/suga/engine/sound/JavaxSoundManager.java b/src/main/java/suga/engine/sound/JavaxSoundManager.java index 85fc750..20cffba 100644 --- a/src/main/java/suga/engine/sound/JavaxSoundManager.java +++ b/src/main/java/suga/engine/sound/JavaxSoundManager.java @@ -4,7 +4,10 @@ import suga.engine.logger.Level; import javax.sound.sampled.*; -import java.io.*; +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; import java.util.HashMap; import java.util.Map; import java.util.Random; diff --git a/src/main/java/suga/engine/threads/AbstractThread.java b/src/main/java/suga/engine/threads/AbstractThread.java new file mode 100644 index 0000000..84136d1 --- /dev/null +++ b/src/main/java/suga/engine/threads/AbstractThread.java @@ -0,0 +1,115 @@ +package suga.engine.threads; + +import suga.engine.GameEngine; +import suga.engine.logger.Level; + +/** + * The AbstractThread implements most of the common elements between threads including setters/getters for pausing, + * frame rate and more. + * + * @author Sugaku + */ +public abstract class AbstractThread extends Thread implements SugaThread { + + /** + * Whether to exit the thread. + */ + protected boolean stopped = false; + + /** + * Whether to simulate game logic or not. + */ + protected boolean paused = false; + + /** + * The target frame rate for this thread. + */ + protected int frameRate; + + /** + * The time that this graphics thread was started. Used in calculating average frame rate. + */ + protected long startTime = 0; + + /** + * The number of frames that have been rendered since the thread started. + */ + protected long frames = 0; + + + /** + * Creates a new thread with the given target rate. + * + * @param frameRate The target frequency to draw frames at. + */ + protected AbstractThread (int frameRate) { + this.frameRate = frameRate; + } + + /** + * Sets the target frame rate of this GraphicsThread. + * + * @param val The new value for the target frame rate. + */ + public void setFrameRate (int val) { + if (val <= 0) { + GameEngine.getInstance().getLogger().log(this.getClass().toString().replaceAll(".+\\.", "") + ": " + val + ". Only natural numbers (no zero) allowed.", Level.EXCEPTION); + return; + } + this.frameRate = val; + } + + /** + * Accessor method for the current target frame rate of the thread. + * + * @return The current target thread refresh rate. + */ + public int getFrameRate () { + return frameRate; + } + + /** + * Sets whether the thread is paused or not. + * + * @param val Whether the thread should be paused or not. + */ + public void setPaused (boolean val) { + paused = val; + } + + /** + * Accessor method for the current status of the thread. + * + * @return Whether the thread is paused currently or not. + */ + public boolean getPaused () { + return paused; + } + + /** + * Sets whether the thread is stopped or not. + * + * @param val Whether the thread should be stopped. + */ + public void setStopped (boolean val) { + stopped = val; + } + + /** + * Accessor method for the current status of the thread. + * + * @return Whether this thread has been stopped or not. + */ + public boolean getStopped () { + return stopped; + } + + /** + * Returns the average frame rate while this GraphicsThread has been running. + * + * @return The average frame rate of this thread since starting. + */ + public double getFPS () { + return (frames * 1.0) / ((System.currentTimeMillis() - startTime) / 1000.0); + } +} diff --git a/src/main/java/suga/engine/threads/GameLogicThread.java b/src/main/java/suga/engine/threads/GameLogicThread.java index fa8a5dd..47a844c 100644 --- a/src/main/java/suga/engine/threads/GameLogicThread.java +++ b/src/main/java/suga/engine/threads/GameLogicThread.java @@ -8,28 +8,13 @@ * * @author Sugaku */ -public class GameLogicThread extends Thread implements SugaThread { - - /** - * Whether to exit the thread. - */ - protected boolean stopped = false; - - /** - * Whether to simulate game logic or not. - */ - protected boolean paused = false; +public class GameLogicThread extends AbstractThread implements SugaThread { /** * The game that should be called once every 1/60th of a second. */ private final Game game; - /** - * A constant on how fast game logic should be called. - */ - private final int LOGIC_RATE; - /** * Creates a new GameLogicThread. * @@ -37,72 +22,39 @@ public class GameLogicThread extends Thread implements SugaThread { * @param rate How many times the logic should be run per second as a maximum. */ public GameLogicThread (Game game, int rate) { + super(rate); this.game = game; - LOGIC_RATE = rate; game.setThread(this); } - /** - * Sets whether the thread is paused or not. - * - * @param val Whether the thread should be paused or not. - */ - public void setPaused (boolean val) { - paused = val; - } - - /** - * Accessor method for the current status of the thread. - * - * @return Whether the thread is paused currently or not. - */ - public boolean getPaused () { - return paused; - } - - /** - * Sets whether the thread is stopped or not. - * - * @param val Whether the thread should be stopped. - */ - public void setStopped (boolean val) { - stopped = val; - } - - /** - * Accessor method for the current status of the thread. - * - * @return Whether this thread has been stopped or not. - */ - public boolean getStopped () { - return stopped; - } - /** * Called to run the Game logic thread. */ @Override public void run () { - long lastFinished = 0; + startTime = System.currentTimeMillis(); + frames = 0; while (!stopped) { - long logicTime = System.currentTimeMillis() - lastFinished; - if (logicTime < (1000 / LOGIC_RATE)) { - try { - //noinspection BusyWait - sleep((1000 / LOGIC_RATE) - logicTime); - } catch (Exception e) { - GameEngine.getInstance().getLogger().log(e); - } - } - lastFinished = System.currentTimeMillis(); + long runtime = 0; game.processInput(); if (!paused) { + long frameStart = System.currentTimeMillis(); try { game.loop(); } catch (Exception e) { GameEngine.getInstance().getLogger().log(e); } + runtime = System.currentTimeMillis() - frameStart; + } + try { + long toWait = Math.round(((1000.0 - ((System.currentTimeMillis() - startTime) % 1000)) / (frameRate - (frames % frameRate) )) - runtime); + if (toWait < 0) toWait = 0; + //noinspection BusyWait + sleep(toWait); + } catch (InterruptedException e) { + GameEngine.getInstance().getLogger().log(e); } + frames++; } } } diff --git a/src/main/java/suga/engine/threads/GraphicsThread.java b/src/main/java/suga/engine/threads/GraphicsThread.java index 9392164..fc62a15 100644 --- a/src/main/java/suga/engine/threads/GraphicsThread.java +++ b/src/main/java/suga/engine/threads/GraphicsThread.java @@ -1,44 +1,19 @@ package suga.engine.threads; import suga.engine.GameEngine; -import suga.engine.graphics.GraphicsPanel; +import suga.engine.graphics.GraphicsPanelInterface; /** * A thread used to refresh the graphics of a panel as fast as possible. * * @author Sugaku */ -public class GraphicsThread extends Thread implements SugaThread { - - /** - * Whether to exit the thread. - */ - protected boolean stopped = false; - - /** - * Whether to simulate game logic or not. - */ - protected boolean paused = false; +public class GraphicsThread extends AbstractThread implements SugaThread { /** * The panel that should be redrawn every frame. */ - private final GraphicsPanel panel; - - /** - * The time that this graphics thread was started. Used in calculating average frame rate. - */ - private static long startTime = 0; - - /** - * The number of frames that have been rendered since the thread started. - */ - private static long frames = 0; - - /** - * The target frame rate for this GraphicsThread. - */ - private final int FRAME_RATE; + private final GraphicsPanelInterface panel; /** * Creates a new graphics thread with the given panel. @@ -46,83 +21,40 @@ public class GraphicsThread extends Thread implements SugaThread { * @param panel The panel to refresh for every frame. * @param frameRate The target frequency to draw frames at. */ - public GraphicsThread (GraphicsPanel panel, int frameRate) { + public GraphicsThread (GraphicsPanelInterface panel, int frameRate) { + super(frameRate); this.panel = panel; - FRAME_RATE = frameRate; + this.frameRate = frameRate; panel.setThread(this); } - /** - * Sets whether the thread is paused or not. - * - * @param val Whether the thread should be paused or not. - */ - public void setPaused (boolean val) { - paused = val; - } - - /** - * Accessor method for the current status of the thread. - * - * @return Whether the thread is paused currently or not. - */ - public boolean getPaused () { - return paused; - } - - /** - * Sets whether the thread is stopped or not. - * - * @param val Whether the thread should be stopped. - */ - public void setStopped (boolean val) { - stopped = val; - } - - /** - * Accessor method for the current status of the thread. - * - * @return Whether this thread has been stopped or not. - */ - public boolean getStopped () { - return stopped; - } - /** * Called to run the Graphics thread. */ @Override public void run () { startTime = System.currentTimeMillis(); - long lastFinished = 0; + frames = 0; while (!stopped) { - long drawTime = System.currentTimeMillis() - lastFinished; - if (drawTime < (1000 / FRAME_RATE)) { - try { - //noinspection BusyWait - sleep((int) ((1000 / FRAME_RATE) - drawTime)); - } catch (Exception e) { - GameEngine.getInstance().getLogger().log(e); - } - } - lastFinished = System.currentTimeMillis(); + long runtime = 0; if (!paused) { + long frameStart = System.currentTimeMillis(); try { panel.repaint(); } catch (Exception e) { GameEngine.getInstance().getLogger().log(e); } + runtime = System.currentTimeMillis() - frameStart; + } + try { + long toWait = Math.round(((1000.0 - ((System.currentTimeMillis() - startTime) % 1000)) / (frameRate - (frames % frameRate) )) - runtime); + if (toWait < 0) toWait = 0; + //noinspection BusyWait + sleep(toWait); + } catch (InterruptedException e) { + GameEngine.getInstance().getLogger().log(e); } frames++; } } - - /** - * Returns the average frame rate while this GraphicsThread has been running. - * - * @return The average frame rate of this thread since starting. - */ - public static double getFPS () { - return (frames * 1.0) / ((System.currentTimeMillis() - startTime) / 1000.0); - } } diff --git a/src/test/java/suga/engine/physics/hitboxes/SquareHitBoxTest.java b/src/test/java/suga/engine/physics/hitboxes/SquareHitBoxTest.java index 0a3c047..16a2b6e 100644 --- a/src/test/java/suga/engine/physics/hitboxes/SquareHitBoxTest.java +++ b/src/test/java/suga/engine/physics/hitboxes/SquareHitBoxTest.java @@ -59,7 +59,7 @@ void isInside () { * Checks if the given point is along the boundary of the hit box or not. Should return false for interior points. */ @ParameterizedTest - @CsvFileSource(resources = "/suga/engine/physics/hitboxes/SquareHitBox/touching.csv", numLinesToSkip = 2, delimiter = ',') + @CsvFileSource(resources = "/suga/engine/physics/hitboxes/SquareHitBox/touching.csv", numLinesToSkip = 1, delimiter = ',') void touching (int width, int height, int cx, int cy, int cz, int x, int y, int z, boolean expected, String reason) { HitBox hitBox = new SquareHitBox(width, height, new Vector(cx, cy, cz)); Vector testPoint = new Vector(x, y, z); diff --git a/src/test/java/suga/engine/threads/GameLogicThreadTest.java b/src/test/java/suga/engine/threads/GameLogicThreadTest.java new file mode 100644 index 0000000..d4ea80e --- /dev/null +++ b/src/test/java/suga/engine/threads/GameLogicThreadTest.java @@ -0,0 +1,59 @@ +package suga.engine.threads; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvFileSource; +import org.mockito.Mockito; +import suga.engine.GameEngine; +import suga.engine.game.Game; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests written for the Game thread. + * + * @author Sugaku + */ +class GameLogicThreadTest { + + /** + * The thread currently under testing. + */ + private GameLogicThread thread; + + /** + * A mock Game without any logic. + */ + private Game game = Mockito.mock(Game.class); + + /** + * Reset the mocks used in the GraphicsThread test. + */ + @BeforeEach + void reset () { + game = Mockito.mock(Game.class); + thread = new GameLogicThread(game, 60); + } + + /** + * Tests whether running the game logic reaches the intended frame rate. + */ // todo perhaps refactor into a k-tail test. + @ParameterizedTest + @CsvFileSource(resources = "/suga/engine/threads/run.csv", numLinesToSkip = 1, delimiter = ',') + void run_PerformsCloseToRequest (int targetFps, long sampleTime) { + final double error = 0.01; // We allow a 1% deviation from the target frame rate. + thread = new GameLogicThread(game, targetFps); + thread.start(); + try { + Thread.sleep(sampleTime * 1000); + } catch (InterruptedException exception) { + fail("Failed to wait for given duration."); + } + thread.setStopped(true); + GameEngine.getInstance().getLogger().log(String.format("Game Thread Test: Wanted %dfps and got %.2ffps. Error is %.2f %%", targetFps, thread.getFPS(), (100 - ((targetFps * 100.0) / thread.getFPS())))); +// assertEquals(targetFps, GraphicsThread.getFPS(), targetFps * error, "Graphics thread should run within " + error + "% of target fps."); + // Results partially depend on test duration and device running them. My testing resulted in a margin of less + // than 1% most often. Regardless no need to fail builds based on the results of this test. + assertTrue(true); + } +} \ No newline at end of file diff --git a/src/test/java/suga/engine/threads/GraphicsThreadTest.java b/src/test/java/suga/engine/threads/GraphicsThreadTest.java new file mode 100644 index 0000000..63b311a --- /dev/null +++ b/src/test/java/suga/engine/threads/GraphicsThreadTest.java @@ -0,0 +1,59 @@ +package suga.engine.threads; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvFileSource; +import org.mockito.Mockito; +import suga.engine.GameEngine; +import suga.engine.graphics.GraphicsPanelInterface; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests written for the GraphicsThread. + * + * @author Sugaku + */ +class GraphicsThreadTest { + + /** + * The thread currently under testing. + */ + private GraphicsThread thread; + + /** + * A mock GraphicsPanelInterface without any logic in .repaint(); + */ + private GraphicsPanelInterface graphicsPanelInterface = Mockito.mock(GraphicsPanelInterface.class); + + /** + * Reset the mocks used in the GraphicsThread test. + */ + @BeforeEach + void reset () { + graphicsPanelInterface = Mockito.mock(GraphicsPanelInterface.class); + thread = new GraphicsThread(graphicsPanelInterface, 60); + } + + /** + * Tests whether running the GraphicsThread reaches the intended frame rate. + */ // todo perhaps refactor into a k-tail test. + @ParameterizedTest + @CsvFileSource(resources = "/suga/engine/threads/run.csv", numLinesToSkip = 1, delimiter = ',') + void run_PerformsCloseToRequest (int targetFps, long sampleTime) { + final double error = 0.01; // We allow a 1% deviation from the target frame rate. + thread = new GraphicsThread(graphicsPanelInterface, targetFps); + thread.start(); + try { + Thread.sleep(sampleTime * 1000); + } catch (InterruptedException exception) { + fail("Failed to wait for given duration."); + } + thread.setStopped(true); + GameEngine.getInstance().getLogger().log(String.format("Graphics Thread Test: Wanted %dfps and got %.2ffps. Error is %.2f %%", targetFps, thread.getFPS(), (100 - ((targetFps * 100.0) / thread.getFPS())))); +// assertEquals(targetFps, GraphicsThread.getFPS(), targetFps * error, "Graphics thread should run within " + error + "% of target fps."); + // Results partially depend on test duration and device running them. My testing resulted in a margin of less + // than 1% most often. Regardless no need to fail builds based on the results of this test. + assertTrue(true); + } +} diff --git a/src/test/resources/suga/engine/threads/run.csv b/src/test/resources/suga/engine/threads/run.csv new file mode 100644 index 0000000..ac83f6c --- /dev/null +++ b/src/test/resources/suga/engine/threads/run.csv @@ -0,0 +1,9 @@ +Target Framerate, Sample Time in Seconds + +60, 10 +120, 10 +144, 10 +30, 10 +50, 10 +25, 10 +13, 10