From 0963869b0ccd60d23a7be51af58300b4c06bac2e Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Mon, 16 Mar 2026 16:01:58 +0100 Subject: [PATCH] Added ability to swap renderers Signed-off-by: Hans Kokx --- .../lib/screens/difficulty_screen.dart | 12 ++- apps/wolf_3d_gui/lib/screens/game_screen.dart | 97 +++++++++++++++++++ .../wolf_3d_renderer/lib/base_renderer.dart | 72 ++++++++++++++ .../lib/wolf_3d_ascii_renderer.dart | 26 ++--- .../lib/wolf_3d_flutter_renderer.dart | 24 ++--- 5 files changed, 197 insertions(+), 34 deletions(-) create mode 100644 apps/wolf_3d_gui/lib/screens/game_screen.dart create mode 100644 packages/wolf_3d_renderer/lib/base_renderer.dart diff --git a/apps/wolf_3d_gui/lib/screens/difficulty_screen.dart b/apps/wolf_3d_gui/lib/screens/difficulty_screen.dart index 1236971..df857f8 100644 --- a/apps/wolf_3d_gui/lib/screens/difficulty_screen.dart +++ b/apps/wolf_3d_gui/lib/screens/difficulty_screen.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:wolf_3d_data_types/wolf_3d_data_types.dart'; import 'package:wolf_3d_flutter/wolf_3d.dart'; -import 'package:wolf_3d_renderer/wolf_3d_flutter_renderer.dart'; +import 'package:wolf_3d_gui/screens/game_screen.dart'; class DifficultyScreen extends StatefulWidget { const DifficultyScreen({ @@ -26,12 +26,18 @@ class _DifficultyScreenState extends State { Navigator.of(context).pushReplacement( MaterialPageRoute( - builder: (_) => WolfFlutterRenderer( - Wolf3d.I.activeGame, + builder: (context) => GameScreen( + data: Wolf3d.I.activeGame, difficulty: difficulty, startingEpisode: Wolf3d.I.activeEpisode, audio: Wolf3d.I.audio, ), + // builder: (_) => WolfFlutterRenderer( + // Wolf3d.I.activeGame, + // difficulty: difficulty, + // startingEpisode: Wolf3d.I.activeEpisode, + // audio: Wolf3d.I.audio, + // ), ), ); } diff --git a/apps/wolf_3d_gui/lib/screens/game_screen.dart b/apps/wolf_3d_gui/lib/screens/game_screen.dart new file mode 100644 index 0000000..22c034a --- /dev/null +++ b/apps/wolf_3d_gui/lib/screens/game_screen.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:wolf_3d_data_types/wolf_3d_data_types.dart'; +import 'package:wolf_3d_engine/wolf_3d_engine.dart'; +import 'package:wolf_3d_flutter/wolf_3d_input_flutter.dart'; +import 'package:wolf_3d_input/wolf_3d_input.dart'; +import 'package:wolf_3d_renderer/wolf_3d_ascii_renderer.dart'; +import 'package:wolf_3d_renderer/wolf_3d_flutter_renderer.dart'; + +class GameScreen extends StatefulWidget { + final WolfensteinData data; + final Difficulty difficulty; + final int startingEpisode; + final EngineAudio audio; + + const GameScreen({ + required this.data, + required this.difficulty, + required this.startingEpisode, + required this.audio, + super.key, + }); + + @override + State createState() => _GameScreenState(); +} + +class _GameScreenState extends State { + late final WolfEngine _engine; + final Wolf3dInput _inputManager = Wolf3dFlutterInput(); + bool _useAsciiMode = false; + + @override + void initState() { + super.initState(); + + // The Host Screen owns the engine lifecycle + _engine = WolfEngine( + data: widget.data, + difficulty: widget.difficulty, + startingEpisode: widget.startingEpisode, + audio: widget.audio, + onGameWon: () => Navigator.of(context).pop(), + ); + + _engine.init(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + // Render the active view + _useAsciiMode + ? WolfAsciiRenderer( + engine: _engine, + inputManager: _inputManager, + ) + : WolfFlutterRenderer( + engine: _engine, + inputManager: _inputManager, + ), + + // Invisible listener to trap the 'Tab' key and swap renderers + Focus( + autofocus: false, + onKeyEvent: (node, event) { + if (event is KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.tab) { + setState(() { + _useAsciiMode = !_useAsciiMode; + }); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + child: const SizedBox.shrink(), + ), + + // Optional: A little UI hint + Positioned( + top: 16, + right: 16, + child: Text( + 'Press TAB to swap renderers', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.5), + fontSize: 12, + ), + ), + ), + ], + ), + ); + } +} diff --git a/packages/wolf_3d_renderer/lib/base_renderer.dart b/packages/wolf_3d_renderer/lib/base_renderer.dart new file mode 100644 index 0000000..33419cf --- /dev/null +++ b/packages/wolf_3d_renderer/lib/base_renderer.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:wolf_3d_engine/wolf_3d_engine.dart'; +import 'package:wolf_3d_input/wolf_3d_input.dart'; + +// 1. The widget now only requires the engine! +abstract class BaseWolfRenderer extends StatefulWidget { + final WolfEngine engine; + final Wolf3dInput inputManager; + + const BaseWolfRenderer({ + required this.engine, + required this.inputManager, + super.key, + }); +} + +abstract class BaseWolfRendererState + extends State + with SingleTickerProviderStateMixin { + late final Ticker gameLoop; + final FocusNode focusNode = FocusNode(); + + Duration _lastTick = Duration.zero; + + @override + void initState() { + super.initState(); + // DO NOT initialize the engine here. Just start the loop! + gameLoop = createTicker(_tick)..start(); + focusNode.requestFocus(); + } + + void _tick(Duration elapsed) { + if (!widget.engine.isInitialized) return; + + Duration delta = elapsed - _lastTick; + _lastTick = elapsed; + + widget.inputManager.update(); + // Tick the shared engine + widget.engine.tick(delta, widget.inputManager.currentInput); + + performRender(); + } + + @override + void dispose() { + gameLoop.dispose(); + focusNode.dispose(); + super.dispose(); + } + + void performRender(); + Widget buildViewport(BuildContext context); + Color get scaffoldColor; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: scaffoldColor, + body: KeyboardListener( + focusNode: focusNode, + autofocus: true, + onKeyEvent: (_) {}, + child: Center( + child: buildViewport(context), + ), + ), + ); + } +} diff --git a/packages/wolf_3d_renderer/lib/wolf_3d_ascii_renderer.dart b/packages/wolf_3d_renderer/lib/wolf_3d_ascii_renderer.dart index 68e38f9..fa4b02f 100644 --- a/packages/wolf_3d_renderer/lib/wolf_3d_ascii_renderer.dart +++ b/packages/wolf_3d_renderer/lib/wolf_3d_ascii_renderer.dart @@ -4,21 +4,15 @@ import 'package:wolf_3d_data_types/wolf_3d_data_types.dart'; import 'package:wolf_3d_engine/wolf_3d_engine.dart'; import 'package:wolf_3d_flutter/wolf_3d_input_flutter.dart'; import 'package:wolf_3d_input/wolf_3d_input.dart'; +import 'package:wolf_3d_renderer/base_renderer.dart'; -class WolfAsciiRenderer extends StatefulWidget { - const WolfAsciiRenderer( - this.data, { - required this.difficulty, - required this.startingEpisode, - required this.audio, +class WolfAsciiRenderer extends BaseWolfRenderer { + const WolfAsciiRenderer({ + required super.engine, + required super.inputManager, super.key, }); - final WolfensteinData data; - final Difficulty difficulty; - final int startingEpisode; - final EngineAudio audio; - @override State createState() => _WolfAsciiRendererState(); } @@ -41,10 +35,10 @@ class _WolfAsciiRendererState extends State super.initState(); engine = WolfEngine( - data: widget.data, - difficulty: widget.difficulty, - startingEpisode: widget.startingEpisode, - audio: widget.audio, + data: widget.engine.data, + difficulty: widget.engine.difficulty, + startingEpisode: widget.engine.startingEpisode, + audio: widget.engine.audio, onGameWon: () { Navigator.of(context).pop(); }, @@ -86,7 +80,7 @@ class _WolfAsciiRendererState extends State } return Scaffold( - backgroundColor: Colors.black, + backgroundColor: Color.fromARGB(255, 4, 64, 64), body: KeyboardListener( focusNode: _focusNode, autofocus: true, diff --git a/packages/wolf_3d_renderer/lib/wolf_3d_flutter_renderer.dart b/packages/wolf_3d_renderer/lib/wolf_3d_flutter_renderer.dart index a6abc8b..04a3afe 100644 --- a/packages/wolf_3d_renderer/lib/wolf_3d_flutter_renderer.dart +++ b/packages/wolf_3d_renderer/lib/wolf_3d_flutter_renderer.dart @@ -6,21 +6,15 @@ import 'package:wolf_3d_data_types/wolf_3d_data_types.dart'; import 'package:wolf_3d_engine/wolf_3d_engine.dart'; import 'package:wolf_3d_flutter/wolf_3d_input_flutter.dart'; import 'package:wolf_3d_input/wolf_3d_input.dart'; +import 'package:wolf_3d_renderer/base_renderer.dart'; -class WolfFlutterRenderer extends StatefulWidget { - const WolfFlutterRenderer( - this.data, { - required this.difficulty, - required this.startingEpisode, - required this.audio, +class WolfFlutterRenderer extends BaseWolfRenderer { + const WolfFlutterRenderer({ + required super.engine, + required super.inputManager, super.key, }); - final WolfensteinData data; - final Difficulty difficulty; - final int startingEpisode; - final EngineAudio audio; - @override State createState() => _WolfFlutterRendererState(); } @@ -47,10 +41,10 @@ class _WolfFlutterRendererState extends State super.initState(); engine = WolfEngine( - data: widget.data, - difficulty: widget.difficulty, - startingEpisode: widget.startingEpisode, - audio: widget.audio, + data: widget.engine.data, + difficulty: widget.engine.difficulty, + startingEpisode: widget.engine.startingEpisode, + audio: widget.engine.audio, onGameWon: () { Navigator.of(context).pop(); },