Migrate to a software rasterizer to dramatically improve performance

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-03-16 00:03:21 +01:00
parent 59fc530a1a
commit 752c143234
15 changed files with 553 additions and 522 deletions

View File

@@ -1,293 +0,0 @@
import 'dart:math';
import 'package:flutter/material.dart';
extension WolfPaletteMatch on Color {
/// Finds the index of the closest color in the wolfPalette
int findClosestIndex(List<Color> palette) {
int closestIndex = 0;
double minDistance = double.infinity;
for (int i = 0; i < palette.length; i++) {
final Color pColor = palette[i];
// Calculate squared Euclidean distance (skipping sqrt for performance)
double distance =
pow(r - pColor.r, 2).toDouble() +
pow(g - pColor.g, 2).toDouble() +
pow(b - pColor.b, 2).toDouble();
if (distance < minDistance) {
minDistance = distance;
closestIndex = i;
}
}
return closestIndex;
}
/// Returns the actual Color object from the palette that matches best
Color toWolfColor(List<Color> palette) {
return palette[findClosestIndex(palette)];
}
}
abstract class ColorPalette {
static const List<Color> vga = [
Color(0xFF000000),
Color(0xFF0000AA),
Color(0xFF00AA00),
Color(0xFF00AAAA),
Color(0xFFAA0000),
Color(0xFFAA00AA),
Color(0xFFAA5500),
Color(0xFFAAAAAA),
Color(0xFF555555),
Color(0xFF5555FF),
Color(0xFF55FF55),
Color(0xFF55FFFF),
Color(0xFFFF5555),
Color(0xFFFF55FF),
Color(0xFFFFFF55),
Color(0xFFFFFFFF),
Color(0xFFEEEEEE),
Color(0xFFDEDEDE),
Color(0xFFD2D2D2),
Color(0xFFC2C2C2),
Color(0xFFB6B6B6),
Color(0xFFAAAAAA),
Color(0xFF999999),
Color(0xFF8D8D8D),
Color(0xFF7D7D7D),
Color(0xFF717171),
Color(0xFF656565),
Color(0xFF555555),
Color(0xFF484848),
Color(0xFF383838),
Color(0xFF2C2C2C),
Color(0xFF202020),
Color(0xFFFF0000),
Color(0xFFEE0000),
Color(0xFFE20000),
Color(0xFFD60000),
Color(0xFFCA0000),
Color(0xFFBE0000),
Color(0xFFB20000),
Color(0xFFA50000),
Color(0xFF990000),
Color(0xFF890000),
Color(0xFF7D0000),
Color(0xFF710000),
Color(0xFF650000),
Color(0xFF590000),
Color(0xFF4C0000),
Color(0xFF400000),
Color(0xFFFFDADA),
Color(0xFFFFBABA),
Color(0xFFFF9D9D),
Color(0xFFFF7D7D),
Color(0xFFFF5D5D),
Color(0xFFFF4040),
Color(0xFFFF2020),
Color(0xFFFF0000),
Color(0xFFFFAA5D),
Color(0xFFFF9940),
Color(0xFFFF8920),
Color(0xFFFF7900),
Color(0xFFE66D00),
Color(0xFFCE6100),
Color(0xFFB65500),
Color(0xFF9D4C00),
Color(0xFFFFFFDA),
Color(0xFFFFFFBA),
Color(0xFFFFFF9D),
Color(0xFFFFFF7D),
Color(0xFFFFFA5D),
Color(0xFFFFF640),
Color(0xFFFFF620),
Color(0xFFFFF600),
Color(0xFFE6DA00),
Color(0xFFCEC600),
Color(0xFFB6AE00),
Color(0xFF9D9D00),
Color(0xFF858500),
Color(0xFF716D00),
Color(0xFF595500),
Color(0xFF404000),
Color(0xFFD2FF5D),
Color(0xFFC6FF40),
Color(0xFFB6FF20),
Color(0xFFA1FF00),
Color(0xFF91E600),
Color(0xFF81CE00),
Color(0xFF75B600),
Color(0xFF619D00),
Color(0xFFDAFFDA),
Color(0xFFBEFFBA),
Color(0xFF9DFF9D),
Color(0xFF81FF7D),
Color(0xFF61FF5D),
Color(0xFF40FF40),
Color(0xFF20FF20),
Color(0xFF00FF00),
Color(0xFF00FF00),
Color(0xFF00EE00),
Color(0xFF00E200),
Color(0xFF00D600),
Color(0xFF04CA00),
Color(0xFF04BE00),
Color(0xFF04B200),
Color(0xFF04A500),
Color(0xFF049900),
Color(0xFF048900),
Color(0xFF047D00),
Color(0xFF047100),
Color(0xFF046500),
Color(0xFF045900),
Color(0xFF044C00),
Color(0xFF044000),
Color(0xFFDAFFFF),
Color(0xFFBAFFFF),
Color(0xFF9DFFFF),
Color(0xFF7DFFFA),
Color(0xFF5DFFFF),
Color(0xFF40FFFF),
Color(0xFF20FFFF),
Color(0xFF00FFFF),
Color(0xFF00E6E6),
Color(0xFF00CECE),
Color(0xFF00B6B6),
Color(0xFF009D9D),
Color(0xFF008585),
Color(0xFF007171),
Color(0xFF005959),
Color(0xFF004040),
Color(0xFF5DBEFF),
Color(0xFF40B2FF),
Color(0xFF20AAFF),
Color(0xFF009DFF),
Color(0xFF008DE6),
Color(0xFF007DCE),
Color(0xFF006DB6),
Color(0xFF005D9D),
Color(0xFFDADADA),
Color(0xFFBABEFF),
Color(0xFF9D9DFF),
Color(0xFF7D81FF),
Color(0xFF5D61FF),
Color(0xFF4040FF),
Color(0xFF2024FF),
Color(0xFF0004FF),
Color(0xFF0000FF),
Color(0xFF0000EE),
Color(0xFF0000E2),
Color(0xFF0000D6),
Color(0xFF0000CA),
Color(0xFF0000BE),
Color(0xFF0000B2),
Color(0xFF0000A5),
Color(0xFF000099),
Color(0xFF000089),
Color(0xFF00007D),
Color(0xFF000071),
Color(0xFF000065),
Color(0xFF000059),
Color(0xFF00004C),
Color(0xFF000040),
Color(0xFF282828),
Color(0xFFFFE234),
Color(0xFFFFD624),
Color(0xFFFFCE18),
Color(0xFFFFC208),
Color(0xFFFFB600),
Color(0xFFB620FF),
Color(0xFFAA00FF),
Color(0xFF9900E6),
Color(0xFF8100CE),
Color(0xFF7500B6),
Color(0xFF61009D),
Color(0xFF500085),
Color(0xFF440071),
Color(0xFF340059),
Color(0xFF280040),
Color(0xFFFFDAFF),
Color(0xFFFFBAFF),
Color(0xFFFF9DFF),
Color(0xFFFF7DFF),
Color(0xFFFF5DFF),
Color(0xFFFF40FF),
Color(0xFFFF20FF),
Color(0xFFFF00FF),
Color(0xFFE200E6),
Color(0xFFCA00CE),
Color(0xFFB600B6),
Color(0xFF9D009D),
Color(0xFF850085),
Color(0xFF6D0071),
Color(0xFF590059),
Color(0xFF400040),
Color(0xFFFFEADE),
Color(0xFFFFE2D2),
Color(0xFFFFDAC6),
Color(0xFFFFD6BE),
Color(0xFFFFCEB2),
Color(0xFFFFC6A5),
Color(0xFFFFBE9D),
Color(0xFFFFBA91),
Color(0xFFFFB281),
Color(0xFFFA571F),
Color(0xFFFF9D61),
Color(0xFFF2955D),
Color(0xFFEA8D59),
Color(0xFFDE8955),
Color(0xFFD28150),
Color(0xFFCA7D4C),
Color(0xFFBE7948),
Color(0xFFB67144),
Color(0xFFAA6940),
Color(0xFFA1653C),
Color(0xFF9D6138),
Color(0xFF915D34),
Color(0xFF895930),
Color(0xFF81502C),
Color(0xFF754C28),
Color(0xFF6D4824),
Color(0xFF5D4020),
Color(0xFF553C1C),
Color(0xFF483818),
Color(0xFF403018),
Color(0xFF382C14),
Color(0xFF28200C),
Color(0xFF610065),
Color(0xFF006565),
Color(0xFF006161),
Color(0xFF00001C),
Color(0xFF00002C),
Color(0xFF302410),
Color(0xFF480048),
Color(0xFF500050),
Color(0xFF000034),
Color(0xFF1C1C1C),
Color(0xFF4C4C4C),
Color(0xFF5D5D5D),
Color(0xFF404040),
Color(0xFF303030),
Color(0xFF343434),
Color(0xFFDAF6F6),
Color(0xFFBAEAEA),
Color(0xFF9DDEDE),
Color(0xFF75CACA),
Color(0xFF48C2C2),
Color(0xFF20B6B6),
Color(0xFF20B2B2),
Color(0xFF00A5A5),
Color(0xFF009999),
Color(0xFF008D8D),
Color(0xFF008585),
Color(0xFF007D7D),
Color(0xFF007979),
Color(0xFF007575),
Color(0xFF007171),
Color(0xFF006D6D),
Color(0xFF990089),
];
}

View File

@@ -1,369 +0,0 @@
import 'dart:math' as math;
import 'package:flutter/material.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_entities/wolf_3d_entities.dart';
import 'package:wolf_3d_renderer/color_palette.dart';
class RaycasterPainter extends CustomPainter {
final Level map;
final List<Sprite> textures;
final Player player;
final double fov;
final Map<String, double> doorOffsets;
final Pushwall? activePushwall;
final List<Sprite> sprites;
final List<Entity> entities;
RaycasterPainter({
required this.map,
required this.textures,
required this.player,
required this.fov,
required this.doorOffsets,
this.activePushwall,
required this.sprites,
required this.entities,
});
@override
void paint(Canvas canvas, Size size) {
final Paint bgPaint = Paint()..isAntiAlias = false;
// 1. Draw Ceiling & Floor
canvas.drawRect(
Rect.fromLTWH(0, 0, size.width, size.height / 2),
bgPaint..color = Colors.blueGrey[900]!,
);
canvas.drawRect(
Rect.fromLTWH(0, size.height / 2, size.width, size.height / 2),
bgPaint..color = Colors.brown[900]!,
);
const int renderWidth = 320;
double columnWidth = size.width / renderWidth;
final Paint columnPaint = Paint()
..isAntiAlias = false
..strokeWidth = columnWidth + 0.5;
List<double> zBuffer = List.filled(renderWidth, 0.0);
Coordinate2D dir = Coordinate2D(
math.cos(player.angle),
math.sin(player.angle),
);
Coordinate2D plane = Coordinate2D(-dir.y, dir.x) * math.tan(fov / 2);
// --- 1. CAST WALLS ---
for (int x = 0; x < renderWidth; x++) {
double cameraX = 2 * x / renderWidth - 1.0;
Coordinate2D rayDir = dir + (plane * cameraX);
int mapX = player.x.toInt();
int mapY = player.y.toInt();
double sideDistX;
double sideDistY;
double deltaDistX = (rayDir.x == 0) ? 1e30 : (1.0 / rayDir.x).abs();
double deltaDistY = (rayDir.y == 0) ? 1e30 : (1.0 / rayDir.y).abs();
double perpWallDist = 0.0;
int stepX;
int stepY;
bool hit = false;
bool hitOutOfBounds = false;
int side = 0;
int hitWallId = 0;
double textureOffset = 0.0; // Replaces doorOffset to handle both
bool customDistCalculated = false; // Flag to skip standard distance
Set<String> ignoredDoors = {};
if (rayDir.x < 0) {
stepX = -1;
sideDistX = (player.x - mapX) * deltaDistX;
} else {
stepX = 1;
sideDistX = (mapX + 1.0 - player.x) * deltaDistX;
}
if (rayDir.y < 0) {
stepY = -1;
sideDistY = (player.y - mapY) * deltaDistY;
} else {
stepY = 1;
sideDistY = (mapY + 1.0 - player.y) * deltaDistY;
}
// DDA Loop
while (!hit) {
if (sideDistX < sideDistY) {
sideDistX += deltaDistX;
mapX += stepX;
side = 0;
} else {
sideDistY += deltaDistY;
mapY += stepY;
side = 1;
}
if (mapY < 0 ||
mapY >= map.length ||
mapX < 0 ||
mapX >= map[0].length) {
hit = true;
hitOutOfBounds = true;
} else if (map[mapY][mapX] > 0) {
String mapKey = '$mapX,$mapY';
// --- DOOR LOGIC ---
if (map[mapY][mapX] >= 90 && !ignoredDoors.contains(mapKey)) {
double currentOffset = doorOffsets[mapKey] ?? 0.0;
if (currentOffset > 0.0) {
double perpWallDistTemp = (side == 0)
? (sideDistX - deltaDistX)
: (sideDistY - deltaDistY);
double wallXTemp = (side == 0)
? player.y + perpWallDistTemp * rayDir.y
: player.x + perpWallDistTemp * rayDir.x;
wallXTemp -= wallXTemp.floor();
if (wallXTemp < currentOffset) {
ignoredDoors.add(mapKey);
continue; // Ray passed through the open door gap
}
}
hit = true;
hitWallId = map[mapY][mapX];
textureOffset = currentOffset;
}
// --- PUSHWALL LOGIC ---
else if (activePushwall != null &&
mapX == activePushwall!.x &&
mapY == activePushwall!.y) {
hit = true;
hitWallId = map[mapY][mapX];
double pOffset = activePushwall!.offset;
int pDirX = activePushwall!.dirX;
int pDirY = activePushwall!.dirY;
perpWallDist = (side == 0)
? (sideDistX - deltaDistX)
: (sideDistY - deltaDistY);
// Did we hit the face that is being pushed deeper?
if (side == 0 && pDirX != 0) {
if (pDirX == stepX) {
double intersect = perpWallDist + pOffset * deltaDistX;
if (intersect < sideDistY) {
perpWallDist = intersect; // Hit the recessed front face
} else {
side =
1; // Missed the front face, hit the newly exposed side!
perpWallDist = sideDistY - deltaDistY;
}
} else {
perpWallDist -= (1.0 - pOffset) * deltaDistX;
}
} else if (side == 1 && pDirY != 0) {
if (pDirY == stepY) {
double intersect = perpWallDist + pOffset * deltaDistY;
if (intersect < sideDistX) {
perpWallDist = intersect;
} else {
side = 0;
perpWallDist = sideDistX - deltaDistX;
}
} else {
perpWallDist -= (1.0 - pOffset) * deltaDistY;
}
} else {
// We hit the side of the sliding block. Did the ray slip behind it?
double wallFraction = (side == 0)
? player.y + perpWallDist * rayDir.y
: player.x + perpWallDist * rayDir.x;
wallFraction -= wallFraction.floor();
if (side == 0) {
if (pDirY == 1 && wallFraction < pOffset) hit = false;
if (pDirY == -1 && wallFraction > (1.0 - pOffset)) hit = false;
if (hit) {
textureOffset =
pOffset * pDirY; // Stick the texture to the block
}
} else {
if (pDirX == 1 && wallFraction < pOffset) hit = false;
if (pDirX == -1 && wallFraction > (1.0 - pOffset)) hit = false;
if (hit) {
textureOffset =
pOffset * pDirX; // Stick the texture to the block
}
}
}
if (!hit) continue; // The ray slipped past! Keep looping.
customDistCalculated = true; // Lock in our custom distance math
}
// --- STANDARD WALL ---
else {
hit = true;
hitWallId = map[mapY][mapX];
}
}
}
if (hitOutOfBounds) continue;
// Apply standard math ONLY if we didn't calculate a sub-tile pushwall distance
if (!customDistCalculated) {
if (side == 0) {
perpWallDist = (sideDistX - deltaDistX);
} else {
perpWallDist = (sideDistY - deltaDistY);
}
}
zBuffer[x] = perpWallDist;
double wallX = (side == 0)
? player.y + perpWallDist * rayDir.y
: player.x + perpWallDist * rayDir.x;
wallX -= wallX.floor();
double drawX = x * columnWidth;
_drawTexturedColumn(
canvas,
drawX,
perpWallDist,
wallX,
side,
size,
hitWallId,
textures,
textureOffset,
columnPaint,
);
}
// --- 2. DRAW SPRITES ---
// (Keep your existing sprite rendering logic exactly the same)
List<Entity> activeSprites = List.from(entities);
activeSprites.sort((a, b) {
double distA = player.position.distanceTo(a.position);
double distB = player.position.distanceTo(b.position);
return distB.compareTo(distA);
});
for (Entity entity in activeSprites) {
Coordinate2D spritePos = entity.position - player.position;
double invDet = 1.0 / (plane.x * dir.y - dir.x * plane.y);
double transformX = invDet * (dir.y * spritePos.x - dir.x * spritePos.y);
double transformY =
invDet * (-plane.y * spritePos.x + plane.x * spritePos.y);
if (transformY > 0) {
int spriteScreenX = ((renderWidth / 2) * (1 + transformX / transformY))
.toInt();
int spriteHeight = (size.height / transformY).abs().toInt();
int spriteColumnWidth = (spriteHeight / columnWidth).toInt();
int drawStartX = -spriteColumnWidth ~/ 2 + spriteScreenX;
int drawEndX = spriteColumnWidth ~/ 2 + spriteScreenX;
int clipStartX = math.max(0, drawStartX);
int clipEndX = math.min(renderWidth - 1, drawEndX);
for (int stripe = clipStartX; stripe < clipEndX; stripe++) {
if (transformY < zBuffer[stripe]) {
double texXDouble = (stripe - drawStartX) * 64 / spriteColumnWidth;
int texX = texXDouble.toInt().clamp(0, 63);
double startY = (size.height / 2) - (spriteHeight / 2);
double stepY = spriteHeight / 64.0;
double drawX = stripe * columnWidth;
int safeIndex = entity.spriteIndex.clamp(0, sprites.length - 1);
Sprite spritePixels = sprites[safeIndex];
for (int ty = 0; ty < 64; ty++) {
int colorByte = spritePixels[texX][ty];
if (colorByte != 255) {
double endY = startY + stepY + 0.5;
if (endY > 0 && startY < size.height) {
columnPaint.color = ColorPalette.vga[colorByte];
canvas.drawLine(
Offset(drawX, startY),
Offset(drawX, endY),
columnPaint,
);
}
}
startY += stepY;
}
}
}
}
}
}
void _drawTexturedColumn(
Canvas canvas,
double drawX,
double distance,
double wallX,
int side,
Size size,
int hitWallId,
List<Sprite> textures,
double textureOffset,
Paint paint,
) {
if (distance <= 0.01) distance = 0.01;
double wallHeight = size.height / distance;
int drawStart = ((size.height / 2) - (wallHeight / 2)).toInt();
int texNum;
int texX;
if (hitWallId >= 90) {
// DOORS
texNum = 98.clamp(0, textures.length - 1);
texX = ((wallX - textureOffset) * 64).toInt().clamp(0, 63);
} else {
// WALLS & PUSHWALLS
texNum = ((hitWallId - 1) * 2).clamp(0, textures.length - 2);
if (side == 1) texNum += 1;
// We apply the modulo % 1.0 to handle negative texture offsets smoothly!
texX = (((wallX - textureOffset) % 1.0) * 64).toInt().clamp(0, 63);
}
if (side == 0 && math.cos(player.angle) > 0) texX = 63 - texX;
if (side == 1 && math.sin(player.angle) < 0) texX = 63 - texX;
double startY = drawStart.toDouble();
double stepY = wallHeight / 64.0;
for (int ty = 0; ty < 64; ty++) {
int colorByte = textures[texNum][texX][ty];
paint.color = ColorPalette.vga[colorByte];
double endY = startY + stepY + 0.5;
if (endY > 0 && startY < size.height) {
canvas.drawLine(Offset(drawX, startY), Offset(drawX, endY), paint);
}
startY += stepY;
}
}
@override
bool shouldRepaint(RaycasterPainter oldDelegate) => true;
}

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:wolf_3d_data_types/wolf_3d_data_types.dart';
import 'package:wolf_3d_renderer/color_palette.dart';
class WeaponPainter extends CustomPainter {
final Sprite? sprite;
@@ -24,11 +23,11 @@ class WeaponPainter extends CustomPainter {
for (int x = 0; x < 64; x++) {
for (int y = 0; y < 64; y++) {
int colorByte = sprite![x][y];
int colorByte = sprite!.pixels[x * 64 + y];
if (colorByte != 255) {
// 255 is our transparent magenta
_paint.color = ColorPalette.vga[colorByte];
_paint.color = Color(ColorPalette.vga32Bit[colorByte]);
canvas.drawRect(
Rect.fromLTWH(

View File

@@ -1,4 +1,4 @@
import 'dart:math' as math;
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
@@ -6,7 +6,6 @@ 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/raycast_painter.dart';
import 'package:wolf_3d_renderer/weapon_painter.dart';
class WolfRenderer extends StatefulWidget {
@@ -29,22 +28,23 @@ class WolfRenderer extends StatefulWidget {
class _WolfRendererState extends State<WolfRenderer>
with SingleTickerProviderStateMixin {
// 1. The input reader
final WolfInput inputManager = WolfInput();
// 2. The central brain of the game
late final WolfEngine engine;
late Ticker _gameLoop;
final FocusNode _focusNode = FocusNode();
final double fov = math.pi / 3;
// --- 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();
// Initialize the engine and hand over all the data and dependencies
engine = WolfEngine(
data: widget.data,
difficulty: widget.difficulty,
@@ -57,33 +57,56 @@ class _WolfRendererState extends State<WolfRenderer>
engine.init();
// Start the loop
_gameLoop = createTicker(_tick)..start();
_focusNode.requestFocus();
}
// --- ORCHESTRATOR ---
void _tick(Duration elapsed) {
// 1. Read the keyboard state
inputManager.update();
if (!engine.isInitialized) return;
// 2. Let the engine do all the math, physics, collision, and logic!
inputManager.update();
engine.tick(elapsed, inputManager.currentInput);
// 3. Force a UI repaint using the newly updated engine state
setState(() {});
// 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) {
// Wait for the engine to finish parsing the level
if (!engine.isInitialized) {
return const Center(child: CircularProgressIndicator(color: Colors.teal));
}
@@ -104,30 +127,18 @@ class _WolfRendererState extends State<WolfRenderer>
aspectRatio: 16 / 10,
child: Stack(
children: [
// --- 3D WORLD ---
// --- 3D WORLD (PIXEL BUFFER) ---
CustomPaint(
size: Size(
constraints.maxWidth,
constraints.maxHeight,
),
painter: RaycasterPainter(
// Read state directly from the engine
map: engine.currentLevel,
textures: widget.data.walls,
player: engine.player,
fov: fov,
doorOffsets: engine.doorManager
.getOffsetsForRenderer(),
entities: engine.entities,
sprites: widget.data.sprites,
activePushwall:
engine.pushwallManager.activePushwall,
),
painter: BufferPainter(_renderedFrame),
),
// --- FIRST PERSON WEAPON ---
Positioned(
bottom: -20,
bottom: 0,
left: 0,
right: 0,
child: Center(
@@ -171,8 +182,6 @@ class _WolfRendererState extends State<WolfRenderer>
},
),
),
// --- UI ---
Hud(player: engine.player),
],
),
@@ -180,3 +189,34 @@ class _WolfRendererState extends State<WolfRenderer>
);
}
}
// --- 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;
}
}