diff --git a/apps/wolf_3d_gui/lib/screens/game_screen.dart b/apps/wolf_3d_gui/lib/screens/game_screen.dart index abb4f6e..5f7c4a4 100644 --- a/apps/wolf_3d_gui/lib/screens/game_screen.dart +++ b/apps/wolf_3d_gui/lib/screens/game_screen.dart @@ -33,10 +33,7 @@ class _GameScreenState extends State { @override void initState() { super.initState(); - _input = Wolf3dFlutterInput(); - - // The Host Screen owns the engine lifecycle _engine = WolfEngine( data: widget.data, difficulty: widget.difficulty, @@ -45,7 +42,6 @@ class _GameScreenState extends State { input: _input, onGameWon: () => Navigator.of(context).pop(), ); - _engine.init(); } @@ -54,20 +50,17 @@ class _GameScreenState extends State { return Scaffold( body: Stack( children: [ - // Render the active view _useAsciiMode ? WolfAsciiRenderer(engine: _engine) : WolfFlutterRenderer(engine: _engine), - // Invisible listener to trap the 'Tab' key and swap renderers + // TAB listener Focus( - autofocus: false, + autofocus: true, onKeyEvent: (node, event) { if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.tab) { - setState(() { - _useAsciiMode = !_useAsciiMode; - }); + setState(() => _useAsciiMode = !_useAsciiMode); return KeyEventResult.handled; } return KeyEventResult.ignored; @@ -75,16 +68,21 @@ class _GameScreenState extends State { child: const SizedBox.shrink(), ), - // Optional: A little UI hint + // Loading Overlay + if (!_engine.isInitialized) + Container( + color: Colors.black, + child: const Center( + child: CircularProgressIndicator(color: Colors.teal), + ), + ), + Positioned( top: 16, right: 16, child: Text( - 'Press TAB to swap renderers', - style: TextStyle( - color: Colors.white.withValues(alpha: 0.5), - fontSize: 12, - ), + 'TAB: Swap Renderer', + style: TextStyle(color: Colors.white.withValues(alpha: 0.5)), ), ), ], diff --git a/packages/wolf_3d_renderer/lib/base_renderer.dart b/packages/wolf_3d_renderer/lib/base_renderer.dart index fbfbacd..7b17d2b 100644 --- a/packages/wolf_3d_renderer/lib/base_renderer.dart +++ b/packages/wolf_3d_renderer/lib/base_renderer.dart @@ -23,7 +23,6 @@ abstract class BaseWolfRendererState @override void initState() { super.initState(); - // DO NOT initialize the engine here. Just start the loop! gameLoop = createTicker(_tick)..start(); focusNode.requestFocus(); } @@ -31,10 +30,14 @@ abstract class BaseWolfRendererState void _tick(Duration elapsed) { if (!widget.engine.isInitialized) return; + if (_lastTick == Duration.zero) { + _lastTick = elapsed; + return; + } + Duration delta = elapsed - _lastTick; _lastTick = elapsed; - // Tick the shared engine widget.engine.tick(delta); performRender(); 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 2e7cc53..5293c8d 100644 --- a/packages/wolf_3d_renderer/lib/wolf_3d_ascii_renderer.dart +++ b/packages/wolf_3d_renderer/lib/wolf_3d_ascii_renderer.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.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_renderer/base_renderer.dart'; @@ -14,81 +13,28 @@ class WolfAsciiRenderer extends BaseWolfRenderer { State createState() => _WolfAsciiRendererState(); } -class _WolfAsciiRendererState extends State - with SingleTickerProviderStateMixin { - late final WolfEngine engine; - late Ticker _gameLoop; - final FocusNode _focusNode = FocusNode(); - - Duration _lastTick = Duration.zero; - - // Changed from String to List> +class _WolfAsciiRendererState extends BaseWolfRendererState { List> _asciiFrame = []; final AsciiRasterizer _asciiRasterizer = AsciiRasterizer(); @override - void initState() { - super.initState(); + Color get scaffoldColor => const Color.fromARGB(255, 4, 64, 64); - engine = WolfEngine( - data: widget.engine.data, - difficulty: widget.engine.difficulty, - startingEpisode: widget.engine.startingEpisode, - audio: widget.engine.audio, - input: widget.engine.input, - onGameWon: () { - Navigator.of(context).pop(); - }, - ); - - engine.init(); - - _gameLoop = createTicker(_tick)..start(); - _focusNode.requestFocus(); - } - - void _tick(Duration elapsed) { - if (!engine.isInitialized) return; - - Duration delta = elapsed - _lastTick; - _lastTick = elapsed; - - engine.tick(delta); - - // Calculate frame synchronously and trigger UI rebuild + @override + void performRender() { setState(() { - // 120x40 is a great sweet spot for text density vs aspect ratio - _asciiFrame = _asciiRasterizer.render(engine, FrameBuffer(160, 100)); + _asciiFrame = _asciiRasterizer.render( + widget.engine, + FrameBuffer(160, 100), + ); }); } @override - void dispose() { - _gameLoop.dispose(); - _focusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - if (!engine.isInitialized) { - return const Center(child: CircularProgressIndicator(color: Colors.teal)); - } - - return Scaffold( - backgroundColor: Color.fromARGB(255, 4, 64, 64), - body: KeyboardListener( - focusNode: _focusNode, - autofocus: true, - onKeyEvent: (_) {}, - // Added the missing argument here - child: Center( - child: _asciiFrame.isEmpty - ? const SizedBox.shrink() - : AsciiFrameWidget(frameData: _asciiFrame), - ), - ), - ); + Widget buildViewport(BuildContext context) { + return _asciiFrame.isEmpty + ? const SizedBox.shrink() + : AsciiFrameWidget(frameData: _asciiFrame); } } 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 1f86a4e..bcbff63 100644 --- a/packages/wolf_3d_renderer/lib/wolf_3d_flutter_renderer.dart +++ b/packages/wolf_3d_renderer/lib/wolf_3d_flutter_renderer.dart @@ -1,7 +1,6 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.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_renderer/base_renderer.dart'; @@ -16,16 +15,8 @@ class WolfFlutterRenderer extends BaseWolfRenderer { State createState() => _WolfFlutterRendererState(); } -class _WolfFlutterRendererState extends State - with SingleTickerProviderStateMixin { - late final WolfEngine engine; - late Ticker _gameLoop; - final FocusNode _focusNode = FocusNode(); - - Duration _lastTick = Duration.zero; - - // --- NEW RASTERIZER STATE --- - // Lock the internal rendering resolution to the classic 320x200 +class _WolfFlutterRendererState + extends BaseWolfRendererState { final FrameBuffer _frameBuffer = FrameBuffer(320, 200); final SoftwareRasterizer _rasterizer = SoftwareRasterizer(); @@ -33,94 +24,49 @@ class _WolfFlutterRendererState extends State bool _isRendering = false; @override - void initState() { - super.initState(); - - engine = WolfEngine( - data: widget.engine.data, - difficulty: widget.engine.difficulty, - startingEpisode: widget.engine.startingEpisode, - audio: widget.engine.audio, - input: widget.engine.input, - onGameWon: () { - Navigator.of(context).pop(); - }, - ); - - engine.init(); - - _gameLoop = createTicker(_tick)..start(); - _focusNode.requestFocus(); - } - - void _tick(Duration elapsed) { - if (!engine.isInitialized) return; - - Duration delta = elapsed - _lastTick; - _lastTick = elapsed; - - engine.tick(delta); - - // Only start rendering a new frame if the previous one is finished. - // This prevents memory leaks and stuttering on lower-end hardware! - if (!_isRendering) { - _isRendering = true; - - // 1. Crunch the math and fill the 1D memory array - _rasterizer.render(engine, _frameBuffer); - - // 2. Convert the raw Uint32List memory into a Flutter ui.Image - ui.decodeImageFromPixels( - // Extract the underlying byte buffer from our 32-bit integer array - _frameBuffer.pixels.buffer.asUint8List(), - _frameBuffer.width, - _frameBuffer.height, - ui.PixelFormat.rgba8888, // Standard 32-bit color format - (ui.Image image) { - if (mounted) { - setState(() { - // ALWAYS dispose the old frame before assigning the new one - // to prevent massive memory leaks on the GPU! - _renderedFrame?.dispose(); - _renderedFrame = image; - }); - } - _isRendering = false; - }, - ); - } - } + Color get scaffoldColor => const Color.fromARGB(255, 4, 64, 64); @override void dispose() { - _gameLoop.dispose(); - _focusNode.dispose(); _renderedFrame?.dispose(); super.dispose(); } @override - Widget build(BuildContext context) { - if (!engine.isInitialized) { - return const Center( - child: CircularProgressIndicator(color: Color.fromARGB(255, 4, 64, 64)), - ); - } + void performRender() { + // Avoid overlapping render calls since decodeImageFromPixels is async + if (_isRendering) return; + _isRendering = true; - return Scaffold( - backgroundColor: Color.fromARGB(255, 4, 64, 64), - body: KeyboardListener( - focusNode: _focusNode, - autofocus: true, - onKeyEvent: (_) {}, - child: Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: AspectRatio( - aspectRatio: 4 / 3, - child: CustomPaint(painter: BufferPainter(_renderedFrame)), - ), - ), + // 1. Rasterize the frame into the pixel buffer + _rasterizer.render(widget.engine, _frameBuffer); + + // 2. Convert raw pixels to a Flutter ui.Image + ui.decodeImageFromPixels( + _frameBuffer.pixels.buffer.asUint8List(), + _frameBuffer.width, + _frameBuffer.height, + ui.PixelFormat.rgba8888, + (ui.Image image) { + if (mounted) { + setState(() { + _renderedFrame?.dispose(); + _renderedFrame = image; + }); + } + _isRendering = false; + }, + ); + } + + @override + Widget buildViewport(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: AspectRatio( + aspectRatio: 4 / 3, + child: CustomPaint( + painter: BufferPainter(_renderedFrame), ), ), ); @@ -129,7 +75,6 @@ class _WolfFlutterRendererState extends State class BufferPainter extends CustomPainter { final ui.Image? frame; - BufferPainter(this.frame); @override @@ -137,7 +82,6 @@ class BufferPainter extends CustomPainter { if (frame == null) return; final Paint paint = Paint()..filterQuality = FilterQuality.none; - final Rect srcRect = Rect.fromLTWH( 0, 0, @@ -150,7 +94,6 @@ class BufferPainter extends CustomPainter { } @override - bool shouldRepaint(covariant BufferPainter oldDelegate) { - return oldDelegate.frame != frame; - } + bool shouldRepaint(covariant BufferPainter oldDelegate) => + oldDelegate.frame != frame; }