diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/EngineSystem.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/EngineSystem.groovy index fe300314..bc26e12f 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/EngineSystem.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/EngineSystem.groovy @@ -19,6 +19,9 @@ package nz.net.ultraq.redhorizon.engine import nz.net.ultraq.redhorizon.engine.scenegraph.Scene import nz.net.ultraq.redhorizon.events.EventTarget +import org.slf4j.Logger +import org.slf4j.LoggerFactory + import groovy.transform.PackageScope /** @@ -33,6 +36,8 @@ import groovy.transform.PackageScope */ abstract class EngineSystem implements Runnable, EventTarget { + private static final Logger logger = LoggerFactory.getLogger(EngineSystem) + private Engine engine protected Scene scene @@ -49,6 +54,14 @@ abstract class EngineSystem implements Runnable, EventTarget { return engine } + /** + * Return the name of the system, for logging purposes. + */ + protected String getSystemName() { + + return this.class.simpleName + } + /** * Execute an action and optionally wait such that, if repeated, it would run * no faster than the given frequency. @@ -68,6 +81,43 @@ abstract class EngineSystem implements Runnable, EventTarget { } } + /** + * Controlled initialization, execution loop, and shutdown or an engine + * system. + */ + @Override + final void run() { + + logger.debug('{}: starting', systemName) + runInit() + trigger(new EngineSystemReadyEvent()) + + logger.debug('{}: in loop', systemName) + runLoop() + + logger.debug('{}: shutting down', systemName) + trigger(new EngineSystemStoppedEvent()) + runShutdown() + logger.debug('{}: stopped', systemName) + } + + /** + * Initialize any resources for the running of the system here. + */ + protected void runInit() { + } + + /** + * Enter the loop for the system to continually work on the scene. + */ + protected abstract void runLoop() + + /** + * Cleanup/free any resources created by the system here. + */ + protected void runShutdown() { + } + /** * Set the engine this system is a part of. */ diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/audio/AudioSystem.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/audio/AudioSystem.groovy index c5ae47d4..899bc72d 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/audio/AudioSystem.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/audio/AudioSystem.groovy @@ -18,8 +18,6 @@ package nz.net.ultraq.redhorizon.engine.audio import nz.net.ultraq.redhorizon.engine.EngineStats import nz.net.ultraq.redhorizon.engine.EngineSystem -import nz.net.ultraq.redhorizon.engine.EngineSystemReadyEvent -import nz.net.ultraq.redhorizon.engine.EngineSystemStoppedEvent import nz.net.ultraq.redhorizon.engine.audio.openal.OpenALContext import nz.net.ultraq.redhorizon.engine.audio.openal.OpenALRenderer import nz.net.ultraq.redhorizon.engine.scenegraph.AudioElement @@ -34,7 +32,7 @@ import java.util.concurrent.LinkedBlockingQueue /** * Audio system, manages the connection to the audio hardware and playback of - * audio objects. + * audio objects in the scene. * * @author Emanuel Rabina */ @@ -47,6 +45,9 @@ class AudioSystem extends EngineSystem implements AudioRequests { private final BlockingQueue>> deletionRequests = new LinkedBlockingQueue<>() private final List queryResults = [] + private OpenALContext context + private OpenALRenderer renderer + /** * Constructor, build a new engine for rendering audio. */ @@ -106,59 +107,54 @@ class AudioSystem extends EngineSystem implements AudioRequests { }) } - /** - * Starts the audio engine loop: builds a connection to the OpenAL device, - * renders audio items found within the current scene, cleaning it all up when - * made to shut down. - */ @Override - void run() { - - logger.debug('Starting audio system') - - // Initialization - new OpenALContext().withCloseable { context -> - context.withCurrent { -> - new OpenALRenderer(config).withCloseable { renderer -> - logger.debug(renderer.toString()) - EngineStats.instance.attachAudioRenderer(renderer) - - trigger(new EngineSystemReadyEvent()) - - // Rendering loop - logger.debug('Audio system in render loop...') - while (!Thread.interrupted()) { - try { - rateLimit(100) { -> - - processRequests(renderer) - - // Run the audio elements - // TODO: Split this out like the graphics system where we wait to - // be told to process audio objects instead of looping - // through the scene ourselves - if (scene?.listener) { - scene.listener.render(renderer) - queryResults.clear() - scene.query(scene.listener.range, queryResults).each { node -> - if (node instanceof AudioElement) { - node.render(renderer) - } - } + protected void runInit() { + + context = new OpenALContext() + context.withCurrent { -> + renderer = new OpenALRenderer(config) + logger.debug(renderer.toString()) + EngineStats.instance.attachAudioRenderer(renderer) + } + } + + @Override + protected void runLoop() { + + context.withCurrent { -> + while (!Thread.interrupted()) { + try { + rateLimit(100) { -> + processRequests(renderer) + + // Run the audio elements + // TODO: Split this out like the graphics system where we wait to + // be told to process audio objects instead of looping + // through the scene ourselves + if (scene?.listener) { + scene.listener.render(renderer) + queryResults.clear() + scene.query(scene.listener.range, queryResults).each { node -> + if (node instanceof AudioElement) { + node.render(renderer) } } } - catch (InterruptedException ignored) { - break - } } } - - // Shutdown - logger.debug('Shutting down audio system') + catch (InterruptedException ignored) { + break + } } } - trigger(new EngineSystemStoppedEvent()) - logger.debug('Audio system stopped') + } + + @Override + protected void runShutdown() { + + context.withCurrent { -> + renderer.close() + } + context.close() } } diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/game/GameLogicSystem.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/game/GameLogicSystem.groovy index dded282a..902ff49e 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/game/GameLogicSystem.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/game/GameLogicSystem.groovy @@ -18,8 +18,6 @@ package nz.net.ultraq.redhorizon.engine.game import nz.net.ultraq.redhorizon.engine.EngineStats import nz.net.ultraq.redhorizon.engine.EngineSystem -import nz.net.ultraq.redhorizon.engine.EngineSystemReadyEvent -import nz.net.ultraq.redhorizon.engine.EngineSystemStoppedEvent import nz.net.ultraq.redhorizon.engine.graphics.GraphicsSystem import org.slf4j.Logger @@ -44,17 +42,10 @@ class GameLogicSystem extends EngineSystem { } @Override - void run() { + void runLoop() { - logger.debug('Starting game logic system') - - // Initialization - // TODO: These phases could probably live in the parent EngineSystem class var graphicsSystem = (GraphicsSystem)engine.systems.find { system -> system instanceof GraphicsSystem } - trigger(new EngineSystemReadyEvent()) - // Game loop - logger.debug('Game logic system in loop...') while (!Thread.interrupted()) { try { var currentTimeMs = System.currentTimeMillis() @@ -71,11 +62,5 @@ class GameLogicSystem extends EngineSystem { break } } - - // Shutdown - logger.debug('Shutting down game logic system') - - trigger(new EngineSystemStoppedEvent()) - logger.debug('Game logic system stopped') } } diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/GraphicsSystem.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/GraphicsSystem.groovy index f3b70bfd..62470abd 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/GraphicsSystem.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/graphics/GraphicsSystem.groovy @@ -18,8 +18,6 @@ package nz.net.ultraq.redhorizon.engine.graphics import nz.net.ultraq.redhorizon.engine.EngineStats import nz.net.ultraq.redhorizon.engine.EngineSystem -import nz.net.ultraq.redhorizon.engine.EngineSystemReadyEvent -import nz.net.ultraq.redhorizon.engine.EngineSystemStoppedEvent import nz.net.ultraq.redhorizon.engine.graphics.imgui.ImGuiLayer import nz.net.ultraq.redhorizon.engine.graphics.opengl.OpenGLContext import nz.net.ultraq.redhorizon.engine.graphics.opengl.OpenGLRenderer @@ -35,7 +33,6 @@ import org.slf4j.LoggerFactory import static org.lwjgl.glfw.GLFW.* import java.util.concurrent.BlockingQueue -import java.util.concurrent.BrokenBarrierException import java.util.concurrent.CompletableFuture import java.util.concurrent.CyclicBarrier import java.util.concurrent.LinkedBlockingQueue @@ -58,7 +55,9 @@ class GraphicsSystem extends EngineSystem implements GraphicsRequests { private final CyclicBarrier renderBarrier = new CyclicBarrier(2) private final CyclicBarrier continueBarrier = new CyclicBarrier(2) + private OpenGLContext context private OpenGLWindow window + private OpenGLRenderer renderer private ImGuiLayer imGuiLayer private RenderPipeline renderPipeline private boolean shouldToggleFullScreen @@ -161,96 +160,93 @@ class GraphicsSystem extends EngineSystem implements GraphicsRequests { }) } - /** - * Start the graphics system loop: creates a new window in which to render the - * elements in the current scene, cleaning it all up when made to shut down. - */ @Override - void run() { + protected void runInit() { - logger.debug('Starting graphics system') + context = new OpenGLContext(windowTitle, config) - var gamepadStateProcessor = new GamepadStateProcessor(inputEventStream) + window = context.window + window.relay(FramebufferSizeEvent, this) + window.relay(WindowMaximizedEvent, this) - // Initialization - new OpenGLContext(windowTitle, config).withCloseable { context -> - window = context.window - window.relay(FramebufferSizeEvent, this) - window.relay(WindowMaximizedEvent, this) - - // Only do quick window mode switching on Windows - the macOS experience - // is quite different from using the fullscreen button which assigns the - // app its own space. - if (System.isWindows()) { - window.on(MouseButtonEvent) { event -> - checkScreenMode(event) - } + // Only do quick window mode switching on Windows - the macOS experience + // is quite different from using the fullscreen button which assigns the + // app its own space. + if (System.isWindows()) { + window.on(MouseButtonEvent) { event -> + checkScreenMode(event) } + } - window.on(KeyEvent) { event -> - if (event.action == GLFW_PRESS && event.key == GLFW_KEY_V) { - shouldToggleVsync = true - } + window.on(KeyEvent) { event -> + if (event.action == GLFW_PRESS && event.key == GLFW_KEY_V) { + shouldToggleVsync = true } + } + + context.withCurrent { -> + trigger(new WindowCreatedEvent(window)) + + renderer = new OpenGLRenderer(config, window) + logger.debug(renderer.toString()) + EngineStats.instance.attachGraphicsRenderer(renderer) + + imGuiLayer = new ImGuiLayer(config, window) + imGuiLayer.relay(FramebufferSizeEvent, this) + + renderPipeline = new RenderPipeline(config, window, renderer, imGuiLayer, inputEventStream) + } + } + + @Override + protected void runLoop() { - context.withCurrent { -> - trigger(new WindowCreatedEvent(window)) - - new OpenGLRenderer(config, window).withCloseable { renderer -> - EngineStats.instance.attachGraphicsRenderer(renderer) - - imGuiLayer = new ImGuiLayer(config, window) - imGuiLayer.withCloseable { imGuiLayer -> - logger.debug(renderer.toString()) - imGuiLayer.relay(FramebufferSizeEvent, this) - - renderPipeline = new RenderPipeline(config, window, renderer, imGuiLayer, inputEventStream) - renderPipeline.withCloseable { pipeline -> - trigger(new EngineSystemReadyEvent()) - - // Rendering loop - logger.debug('Graphics system in render loop...') - while (!window.shouldClose() && !Thread.interrupted()) { - try { - if (shouldToggleFullScreen) { - window.toggleFullScreen() - shouldToggleFullScreen = false - } - if (shouldToggleVsync) { - window.toggleVsync() - shouldToggleVsync = false - } - processRequests(renderer) - - // Synchronization point where we wait for the game logic - // system to signal that it's OK to gather renderables from - // the scene. Once we have those renderables, we can let the - // game logic system continue while we draw those out. - renderBarrier.await() - renderBarrier.reset() - pipeline.gather() - continueBarrier.await() - continueBarrier.reset() - - pipeline.render() - window.swapBuffers() - window.pollEvents() - gamepadStateProcessor.process() - } - catch (BrokenBarrierException | InterruptedException ignored) { - break - } - } - - // Shutdown - logger.debug('Shutting down graphics system') - } + var gamepadStateProcessor = new GamepadStateProcessor(inputEventStream) + + context.withCurrent { -> + while (!window.shouldClose() && !Thread.interrupted()) { + try { + if (shouldToggleFullScreen) { + window.toggleFullScreen() + shouldToggleFullScreen = false + } + if (shouldToggleVsync) { + window.toggleVsync() + shouldToggleVsync = false } + processRequests(renderer) + + // Synchronization point where we wait for the game logic + // system to signal that it's OK to gather renderables from + // the scene. Once we have those renderables, we can let the + // game logic system continue while we draw those out. + renderBarrier.await() + renderBarrier.reset() + renderPipeline.gather() + continueBarrier.await() + continueBarrier.reset() + + renderPipeline.render() + window.swapBuffers() + window.pollEvents() + gamepadStateProcessor.process() + } + catch (InterruptedException ignored) { + break } } } - trigger(new EngineSystemStoppedEvent()) - logger.debug('Graphics system stopped') + } + + @Override + protected void runShutdown() { + + context.withCurrent { -> + renderPipeline.close() + imGuiLayer.close() + renderer.close() + } + context.close() } /** diff --git a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/time/TimeSystem.groovy b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/time/TimeSystem.groovy index d5af6115..496331fa 100644 --- a/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/time/TimeSystem.groovy +++ b/redhorizon-engine/source/nz/net/ultraq/redhorizon/engine/time/TimeSystem.groovy @@ -17,15 +17,14 @@ package nz.net.ultraq.redhorizon.engine.time import nz.net.ultraq.redhorizon.engine.EngineSystem -import nz.net.ultraq.redhorizon.engine.EngineSystemReadyEvent -import nz.net.ultraq.redhorizon.engine.EngineSystemStoppedEvent import org.slf4j.Logger import org.slf4j.LoggerFactory /** * A separate time source from the usual system time, allowing game time to flow - * at different speeds. + * at different speeds. This system is used to update any temporal components + * in the scene. * * @author Emanuel Rabina */ @@ -33,6 +32,8 @@ class TimeSystem extends EngineSystem { private static Logger logger = LoggerFactory.getLogger(TimeSystem) + private long lastSystemTimeMillis + private long currentTimeMillis private float speed = 1.0f private float lastSpeed @@ -42,6 +43,13 @@ class TimeSystem extends EngineSystem { scene.gameClock = this } + @Override + protected void runInit() { + + lastSystemTimeMillis = System.currentTimeMillis() + currentTimeMillis = lastSystemTimeMillis + } + /** * Return whether or not time has been paused. * @@ -71,20 +79,9 @@ class TimeSystem extends EngineSystem { speed = lastSpeed } - /** - * Starts the game clock and uses it to update temporal objects in the scene. - */ @Override - void run() { - - logger.debug('Starting time system') - - trigger(new EngineSystemReadyEvent()) + protected void runLoop() { - var lastSystemTimeMillis = System.currentTimeMillis() - long currentTimeMillis = lastSystemTimeMillis - - logger.debug('Time system in update loop') while (!Thread.interrupted()) { try { rateLimit(100) { -> @@ -110,9 +107,6 @@ class TimeSystem extends EngineSystem { break } } - - trigger(new EngineSystemStoppedEvent()) - logger.debug('Time system stopped') } /**