From 3c8fc40cc23a6824ebbf22b52cefed2fbc8af382 Mon Sep 17 00:00:00 2001 From: Emanuel Rabina Date: Sat, 28 Dec 2024 13:28:43 +1300 Subject: [PATCH] WIP --- .../redhorizon/engine/EngineSystem.groovy | 50 +++++ .../engine/audio/AudioSystem.groovy | 104 +++++----- .../engine/game/GameLogicSystem.groovy | 22 +-- .../engine/graphics/GraphicsSystem.groovy | 177 +++++++++--------- .../redhorizon/engine/time/TimeSystem.groovy | 46 ++--- 5 files changed, 213 insertions(+), 186 deletions(-) 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..0d8e9ec0 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,25 @@ abstract class EngineSystem implements Runnable, EventTarget { return engine } + /** + * Return the name of the system, for logging purposes. + */ + protected String getSystemName() { + + return this.class.simpleName + } + + /** + * Initialize any resources for the running of the system here. + */ + protected void init() { + } + + /** + * Enter the loop for the system to continually work on the scene. + */ + protected abstract void processLoop() + /** * Execute an action and optionally wait such that, if repeated, it would run * no faster than the given frequency. @@ -68,6 +92,26 @@ abstract class EngineSystem implements Runnable, EventTarget { } } + /** + * Controlled initialization, execution loop, and shutdown or an engine + * system. + */ + @Override + final void run() { + + logger.debug('{}: starting', systemName) + init() + trigger(new EngineSystemReadyEvent()) + + logger.debug('{}: in loop', systemName) + processLoop() + + logger.debug('{}: shutting down', systemName) + trigger(new EngineSystemStoppedEvent()) + shutdown() + logger.debug('{}: stopped', systemName) + } + /** * Set the engine this system is a part of. */ @@ -86,4 +130,10 @@ abstract class EngineSystem implements Runnable, EventTarget { this.scene = scene configureScene() } + + /** + * Cleanup/free any resources created by the system here. + */ + protected void shutdown() { + } } 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..c36ba3a6 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. */ @@ -61,6 +62,48 @@ class AudioSystem extends EngineSystem implements AudioRequests { scene.audioRequestsHandler = this } + @Override + protected void init() { + + context = new OpenALContext() + context.withCurrent { -> + renderer = new OpenALRenderer(config) + logger.debug(renderer.toString()) + EngineStats.instance.attachAudioRenderer(renderer) + } + } + + @Override + protected void processLoop() { + + 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 + } + } + } + } + /** * Run through all of the queued requests for the creation and deletion of * graphics resources. @@ -106,59 +149,12 @@ 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) - } - } - } - } - } - catch (InterruptedException ignored) { - break - } - } - } + protected void shutdown() { - // Shutdown - logger.debug('Shutting down audio system') - } + context.withCurrent { -> + renderer.close() } - trigger(new EngineSystemStoppedEvent()) - logger.debug('Audio system stopped') + 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..abec97a0 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 @@ -35,6 +33,7 @@ class GameLogicSystem extends EngineSystem { private static final Logger logger = LoggerFactory.getLogger(GameLogicSystem) + private GraphicsSystem graphicsSystem private long lastUpdateTimeMs @Override @@ -44,17 +43,14 @@ class GameLogicSystem extends EngineSystem { } @Override - void run() { + protected void init() { - logger.debug('Starting game logic system') + graphicsSystem = (GraphicsSystem)engine.systems.find { system -> system instanceof GraphicsSystem } + } - // 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()) + @Override + void processLoop() { - // Game loop - logger.debug('Game logic system in loop...') while (!Thread.interrupted()) { try { var currentTimeMs = System.currentTimeMillis() @@ -71,11 +67,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..cd41f264 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,10 @@ class GraphicsSystem extends EngineSystem implements GraphicsRequests { private final CyclicBarrier renderBarrier = new CyclicBarrier(2) private final CyclicBarrier continueBarrier = new CyclicBarrier(2) + private GamepadStateProcessor gamepadStateProcessor + private OpenGLContext context private OpenGLWindow window + private OpenGLRenderer renderer private ImGuiLayer imGuiLayer private RenderPipeline renderPipeline private boolean shouldToggleFullScreen @@ -111,6 +111,84 @@ class GraphicsSystem extends EngineSystem implements GraphicsRequests { return imGuiLayer } + @Override + protected void init() { + + gamepadStateProcessor = new GamepadStateProcessor(inputEventStream) + + context = new OpenGLContext(windowTitle, config) + + 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) + } + } + + 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 processLoop() { + + 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 + } + } + } + } + /** * Run through all of the queued requests for the creation and deletion of * graphics resources. @@ -161,96 +239,15 @@ 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() { - - logger.debug('Starting graphics system') - - var gamepadStateProcessor = new GamepadStateProcessor(inputEventStream) + protected void shutdown() { - // 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) - } - } - - window.on(KeyEvent) { event -> - if (event.action == GLFW_PRESS && event.key == GLFW_KEY_V) { - shouldToggleVsync = true - } - } - - 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') - } - } - } - } + context.withCurrent { -> + renderPipeline.close() + imGuiLayer.close() + renderer.close() } - trigger(new EngineSystemStoppedEvent()) - logger.debug('Graphics system stopped') + 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..1f66df91 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 init() { + + lastSystemTimeMillis = System.currentTimeMillis() + currentTimeMillis = lastSystemTimeMillis + } + /** * Return whether or not time has been paused. * @@ -62,29 +70,9 @@ class TimeSystem extends EngineSystem { speed = 0.0f } - /** - * Resume the flow of time. - */ - void resume() { - - logger.debug('Resuming game clock') - 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 processLoop() { - var lastSystemTimeMillis = System.currentTimeMillis() - long currentTimeMillis = lastSystemTimeMillis - - logger.debug('Time system in update loop') while (!Thread.interrupted()) { try { rateLimit(100) { -> @@ -110,9 +98,15 @@ class TimeSystem extends EngineSystem { break } } + } - trigger(new EngineSystemStoppedEvent()) - logger.debug('Time system stopped') + /** + * Resume the flow of time. + */ + void resume() { + + logger.debug('Resuming game clock') + speed = lastSpeed } /**