Files
wolf_dart/packages/wolf_3d_renderer/lib/wolf_3d_renderer.dart

162 lines
4.2 KiB
Dart

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_flutter/wolf_3d_input_flutter.dart';
import 'package:wolf_3d_input/wolf_3d_input.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<WolfRenderer> createState() => _WolfRendererState();
}
class _WolfRendererState extends State<WolfRenderer>
with SingleTickerProviderStateMixin {
final Wolf3dInput inputManager = Wolf3dFlutterInput();
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: Color.fromARGB(255, 4, 64, 64)),
);
}
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)),
),
),
),
),
);
}
}
class BufferPainter extends CustomPainter {
final ui.Image? frame;
BufferPainter(this.frame);
@override
void paint(Canvas canvas, Size size) {
if (frame == null) return;
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;
}
}