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

223 lines
6.8 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_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<WolfRenderer> createState() => _WolfRendererState();
}
class _WolfRendererState extends State<WolfRenderer>
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;
}
}