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_input/wolf_3d_input.dart'; import 'package:wolf_3d_renderer/hud.dart'; import 'package:wolf_3d_renderer/weapon_painter.dart'; class WolfRenderer extends StatefulWidget { const WolfRenderer( this.data, { required this.difficulty, required this.startingEpisode, required this.audio, super.key, }); final WolfensteinData data; final Difficulty difficulty; final int startingEpisode; final EngineAudio audio; @override State createState() => _WolfRendererState(); } class _WolfRendererState extends State with SingleTickerProviderStateMixin { final WolfInput inputManager = WolfInput(); late final WolfEngine engine; late Ticker _gameLoop; final FocusNode _focusNode = FocusNode(); // --- NEW RASTERIZER STATE --- // Lock the internal rendering resolution to the classic 320x200 final FrameBuffer _frameBuffer = FrameBuffer(320, 200); final SoftwareRasterizer _rasterizer = SoftwareRasterizer(); ui.Image? _renderedFrame; bool _isRendering = false; @override void initState() { super.initState(); engine = WolfEngine( data: widget.data, difficulty: widget.difficulty, startingEpisode: widget.startingEpisode, audio: widget.audio, onGameWon: () { Navigator.of(context).pop(); }, ); engine.init(); _gameLoop = createTicker(_tick)..start(); _focusNode.requestFocus(); } void _tick(Duration elapsed) { if (!engine.isInitialized) return; inputManager.update(); engine.tick(elapsed, inputManager.currentInput); // 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; }, ); } } @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: Colors.teal)); } return Scaffold( backgroundColor: Colors.black, body: KeyboardListener( focusNode: _focusNode, autofocus: true, onKeyEvent: (_) {}, child: Column( children: [ Expanded( child: LayoutBuilder( builder: (context, constraints) { return Center( child: AspectRatio( aspectRatio: 16 / 10, child: Stack( children: [ // --- 3D WORLD (PIXEL BUFFER) --- CustomPaint( size: Size( constraints.maxWidth, constraints.maxHeight, ), painter: BufferPainter(_renderedFrame), ), // --- FIRST PERSON WEAPON --- Positioned( bottom: 0, left: 0, right: 0, child: Center( child: Transform.translate( offset: Offset( 0, engine.player.weaponAnimOffset, ), child: SizedBox( width: 500, height: 500, child: CustomPaint( painter: WeaponPainter( sprite: widget.data.sprites[engine .player .currentWeapon .getCurrentSpriteIndex( widget.data.sprites.length, )], ), ), ), ), ), ), // --- DAMAGE FLASH --- if (engine.damageFlashOpacity > 0) Positioned.fill( child: Container( color: Colors.red.withValues( alpha: engine.damageFlashOpacity, ), ), ), ], ), ), ); }, ), ), Hud(player: engine.player), ], ), ), ); } } // --- DEAD SIMPLE PAINTER --- // It literally just stretches the 320x200 image to fill the screen class BufferPainter extends CustomPainter { final ui.Image? frame; BufferPainter(this.frame); @override void paint(Canvas canvas, Size size) { if (frame == null) return; // FilterQuality.none guarantees the classic, chunky, un-blurred pixels! final Paint paint = Paint()..filterQuality = FilterQuality.none; final Rect srcRect = Rect.fromLTWH( 0, 0, frame!.width.toDouble(), frame!.height.toDouble(), ); final Rect dstRect = Rect.fromLTWH(0, 0, size.width, size.height); canvas.drawImageRect(frame!, srcRect, dstRect, paint); } @override bool shouldRepaint(covariant BufferPainter oldDelegate) { return oldDelegate.frame != frame; } }