diff --git a/lib/screens/sprite_gallery.dart b/lib/screens/sprite_gallery.dart index 1ef87bf..5498f29 100644 --- a/lib/screens/sprite_gallery.dart +++ b/lib/screens/sprite_gallery.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:wolf_3d_data_types/wolf_3d_data_types.dart'; import 'package:wolf_3d_entities/wolf_3d_entities.dart'; import 'package:wolf_3d_flutter/wolf_3d.dart'; -import 'package:wolf_3d_renderer/color_palette.dart'; class SpriteGallery extends StatelessWidget { final List sprites; @@ -76,12 +75,12 @@ class SingleSpritePainter extends CustomPainter { double pixelSize = size.width / 64; 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) { // Skip transparency canvas.drawRect( Rect.fromLTWH(x * pixelSize, y * pixelSize, pixelSize, pixelSize), - Paint()..color = ColorPalette.vga[colorByte], + Paint()..color = Color(ColorPalette.vga32Bit[colorByte]), ); } } diff --git a/packages/wolf_3d_data/lib/src/wl_parser.dart b/packages/wolf_3d_data/lib/src/wl_parser.dart index 4ace6cb..5a1fccb 100644 --- a/packages/wolf_3d_data/lib/src/wl_parser.dart +++ b/packages/wolf_3d_data/lib/src/wl_parser.dart @@ -180,32 +180,33 @@ abstract class WLParser { // --- Private Helpers --- static Sprite _parseWallChunk(ByteData vswap, int offset) { - // Generate the 64x64 pixel grid in column-major order functionally - return List.generate( - 64, - (x) => List.generate(64, (y) => vswap.getUint8(offset + (x * 64) + y)), - ); + final pixels = Uint8List(64 * 64); + for (int x = 0; x < 64; x++) { + for (int y = 0; y < 64; y++) { + // Flat 1D index: x * 64 + y + pixels[x * 64 + y] = vswap.getUint8(offset + (x * 64) + y); + } + } + return Sprite(pixels); } static Sprite _parseSingleSprite(ByteData vswap, int offset) { - // Initialize the 64x64 grid with 255 (The Magenta Transparency Color!) - Sprite sprite = List.generate(64, (_) => List.filled(64, 255)); + // Initialize the 1D array with 255 (Transparency) + final pixels = Uint8List(64 * 64)..fillRange(0, 4096, 255); + Sprite sprite = Sprite(pixels); int leftPix = vswap.getUint16(offset, Endian.little); int rightPix = vswap.getUint16(offset + 2, Endian.little); - // Parse vertical columns within the sprite bounds for (int x = leftPix; x <= rightPix; x++) { int colOffset = vswap.getUint16( offset + 4 + ((x - leftPix) * 2), Endian.little, ); - if (colOffset != 0) { _parseSpriteColumn(vswap, sprite, x, offset, offset + colOffset); } } - return sprite; } @@ -216,23 +217,21 @@ abstract class WLParser { int baseOffset, int cmdOffset, ) { - // Execute the column drawing commands while (true) { int endY = vswap.getUint16(cmdOffset, Endian.little); - if (endY == 0) break; // 0 marks the end of the column - endY ~/= 2; // Wolf3D stores Y coordinates multiplied by 2 + if (endY == 0) break; + endY ~/= 2; int pixelOfs = vswap.getUint16(cmdOffset + 2, Endian.little); - int startY = vswap.getUint16(cmdOffset + 4, Endian.little); startY ~/= 2; for (int y = startY; y < endY; y++) { - // The Carmack 286 Hack: pixelOfs + y gives the exact byte address - sprite[x][y] = vswap.getUint8(baseOffset + pixelOfs + y); + // Write directly to the 1D array + sprite.pixels[x * 64 + y] = vswap.getUint8(baseOffset + pixelOfs + y); } - cmdOffset += 6; // Move to the next 6-byte instruction + cmdOffset += 6; } } diff --git a/packages/wolf_3d_data_types/lib/src/color_palette.dart b/packages/wolf_3d_data_types/lib/src/color_palette.dart new file mode 100644 index 0000000..fb65c5b --- /dev/null +++ b/packages/wolf_3d_data_types/lib/src/color_palette.dart @@ -0,0 +1,262 @@ +import 'dart:typed_data'; + +abstract class ColorPalette { + static final Uint32List vga32Bit = Uint32List.fromList([ + 0xFF000000, + 0xFFAA0000, + 0xFF00AA00, + 0xFFAAAA00, + 0xFF0000AA, + 0xFFAA00AA, + 0xFF0055AA, + 0xFFAAAAAA, + 0xFF555555, + 0xFFFF5555, + 0xFF55FF55, + 0xFFFFFF55, + 0xFF5555FF, + 0xFFFF55FF, + 0xFF55FFFF, + 0xFFFFFFFF, + 0xFFEEEEEE, + 0xFFDEDEDE, + 0xFFD2D2D2, + 0xFFC2C2C2, + 0xFFB6B6B6, + 0xFFAAAAAA, + 0xFF999999, + 0xFF8D8D8D, + 0xFF7D7D7D, + 0xFF717171, + 0xFF656565, + 0xFF555555, + 0xFF484848, + 0xFF383838, + 0xFF2C2C2C, + 0xFF202020, + 0xFF0000FF, + 0xFF0000EE, + 0xFF0000E2, + 0xFF0000D6, + 0xFF0000CA, + 0xFF0000BE, + 0xFF0000B2, + 0xFF0000A5, + 0xFF000099, + 0xFF000089, + 0xFF00007D, + 0xFF000071, + 0xFF000065, + 0xFF000059, + 0xFF00004C, + 0xFF000040, + 0xFFDADAFF, + 0xFFBABAFF, + 0xFF9D9DFF, + 0xFF7D7DFF, + 0xFF5D5DFF, + 0xFF4040FF, + 0xFF2020FF, + 0xFF0000FF, + 0xFF5DAAFF, + 0xFF4099FF, + 0xFF2089FF, + 0xFF0079FF, + 0xFF006DE6, + 0xFF0061CE, + 0xFF0055B6, + 0xFF004C9D, + 0xFFDAFFFF, + 0xBAFFFF, + 0x9DFFFF, + 0x7DFFFF, + 0x5DFAFF, + 0x40F6FF, + 0x20F6FF, + 0x00F6FF, + 0x00DAE6, + 0x00C6CE, + 0x00AEB6, + 0x009D9D, + 0x008585, + 0x006D71, + 0x005559, + 0x004040, + 0x5DFFD2, + 0x40FFC6, + 0x20FFB6, + 0x00FFA1, + 0x00E691, + 0x00CE81, + 0x00B675, + 0x009D61, + 0xDAFFDA, + 0xBAFFBE, + 0x9DFF9D, + 0x7DFF81, + 0x5DFF61, + 0x40FF40, + 0x20FF20, + 0x00FF00, + 0x00FF00, + 0x00EE00, + 0x00E200, + 0x00D600, + 0x00CA04, + 0x00BE04, + 0x00B204, + 0x00A504, + 0x009904, + 0x008904, + 0x007D04, + 0x007104, + 0x006504, + 0x005904, + 0x004C04, + 0x004004, + 0xFFFFDA, + 0xFFFFBA, + 0xFFFF9D, + 0xFAFF7D, + 0xFFFF5D, + 0xFFFF40, + 0xFFFF20, + 0xFFFF00, + 0xE6E600, + 0xCECE00, + 0xB6B600, + 0x9D9D00, + 0x858500, + 0x717100, + 0x595900, + 0x404000, + 0xFFBE5D, + 0xFFB240, + 0xFFAA20, + 0xFF9D00, + 0xE68D00, + 0xCE7D00, + 0xB66D00, + 0x9D5D00, + 0xDADADA, + 0xFFBEBA, + 0xFF9D9D, + 0xFF817D, + 0xFF615D, + 0xFF4040, + 0xFF2420, + 0xFF0400, + 0xFF0000, + 0xEE0000, + 0xE20000, + 0xD60000, + 0xCA0000, + 0xBE0000, + 0xB20000, + 0xA50000, + 0x990000, + 0x890000, + 0x7D0000, + 0x710000, + 0x650000, + 0x590000, + 0x4C0000, + 0x400000, + 0x282828, + 0x34E2FF, + 0x24D6FF, + 0x18CEFF, + 0x08C2FF, + 0x00B6FF, + 0xFF20B6, + 0xFF00AA, + 0xE60099, + 0xCE0081, + 0xB60075, + 0x9D0061, + 0x850050, + 0x710044, + 0x590034, + 0x400028, + 0xFFDAFF, + 0xFFBAFF, + 0xFF9DFF, + 0xFF7DFF, + 0xFF5DFF, + 0xFF40FF, + 0xFF20FF, + 0xFF00FF, + 0xE600E2, + 0xCE00CA, + 0xB600B6, + 0x9D009D, + 0x850085, + 0x71006D, + 0x590059, + 0x400040, + 0xDEEAFF, + 0xD2E2FF, + 0xC6DAFF, + 0xBED6FF, + 0xB2CEFF, + 0xA5C6FF, + 0x9DBEFF, + 0x91BAFF, + 0x81B2FF, + 0x1F57FA, + 0x619DFF, + 0x5D95F2, + 0x598DEA, + 0x5589DE, + 0x5081D2, + 0x4C7DCA, + 0x4879BE, + 0x4471B6, + 0x4069AA, + 0x3C65A1, + 0x38619D, + 0x345D91, + 0x305989, + 0x2C5081, + 0x284C75, + 0x24486D, + 0x20405D, + 0x1C3C55, + 0x183848, + 0x183040, + 0x142C38, + 0x0C2028, + 0x650061, + 0x656500, + 0x616100, + 0x1C0000, + 0x2C0000, + 0x102430, + 0x480048, + 0x500050, + 0x340000, + 0x1C1C1C, + 0x4C4C4C, + 0x5D5D5D, + 0x404040, + 0x303030, + 0x343434, + 0xF6F6DA, + 0xEAEABA, + 0xDEDED9, + 0xCACA75, + 0xC2C248, + 0xB6B620, + 0xB2B220, + 0xA5A500, + 0x999900, + 0x8D8D00, + 0x858500, + 0x7D7D00, + 0x797900, + 0x757500, + 0x717100, + 0x6D6D00, + 0x890099, + ]); +} diff --git a/packages/wolf_3d_data_types/lib/src/frame_buffer.dart b/packages/wolf_3d_data_types/lib/src/frame_buffer.dart new file mode 100644 index 0000000..ba7fc37 --- /dev/null +++ b/packages/wolf_3d_data_types/lib/src/frame_buffer.dart @@ -0,0 +1,19 @@ +import 'dart:typed_data'; + +class FrameBuffer { + final int width; + final int height; + + // A 1D array representing the 2D screen. + // Length = width * height. + final Uint32List pixels; + + FrameBuffer(this.width, this.height) : pixels = Uint32List(width * height); + + // Helper to clear the screen (e.g., draw ceiling and floor) + void clear(int ceilingColor32, int floorColor32) { + int half = (width * height) ~/ 2; + pixels.fillRange(0, half, ceilingColor32); + pixels.fillRange(half, pixels.length, floorColor32); + } +} diff --git a/packages/wolf_3d_data_types/lib/src/sprite.dart b/packages/wolf_3d_data_types/lib/src/sprite.dart index cbdab04..e63c02e 100644 --- a/packages/wolf_3d_data_types/lib/src/sprite.dart +++ b/packages/wolf_3d_data_types/lib/src/sprite.dart @@ -1,7 +1,21 @@ +import 'dart:typed_data'; + typedef Matrix = List>; +typedef SpriteMap = Matrix; -typedef Sprite = Matrix; +class Sprite { + final Uint8List pixels; -typedef Wall = Sprite; + Sprite(this.pixels); -typedef Level = Matrix; + // Factory to convert your 2D matrices into a 1D array during load time + factory Sprite.fromMatrix(Matrix matrix) { + final pixels = Uint8List(64 * 64); + for (int y = 0; y < 64; y++) { + for (int x = 0; x < 64; x++) { + pixels[x * 64 + y] = matrix[x][y]; + } + } + return Sprite(pixels); + } +} diff --git a/packages/wolf_3d_data_types/lib/src/wolf_level.dart b/packages/wolf_3d_data_types/lib/src/wolf_level.dart index e405f75..06b9bd7 100644 --- a/packages/wolf_3d_data_types/lib/src/wolf_level.dart +++ b/packages/wolf_3d_data_types/lib/src/wolf_level.dart @@ -2,8 +2,8 @@ import 'package:wolf_3d_data_types/wolf_3d_data_types.dart'; class WolfLevel { final String name; - final Sprite wallGrid; - final Sprite objectGrid; + final SpriteMap wallGrid; + final SpriteMap objectGrid; final int musicIndex; const WolfLevel({ diff --git a/packages/wolf_3d_data_types/lib/wolf_3d_data_types.dart b/packages/wolf_3d_data_types/lib/wolf_3d_data_types.dart index 3be1a53..d54618d 100644 --- a/packages/wolf_3d_data_types/lib/wolf_3d_data_types.dart +++ b/packages/wolf_3d_data_types/lib/wolf_3d_data_types.dart @@ -4,10 +4,12 @@ library; export 'src/cardinal_direction.dart' show CardinalDirection; +export 'src/color_palette.dart' show ColorPalette; export 'src/coordinate_2d.dart' show Coordinate2D; export 'src/difficulty.dart' show Difficulty; export 'src/enemy_map_data.dart' show EnemyMapData; export 'src/episode.dart' show Episode; +export 'src/frame_buffer.dart' show FrameBuffer; export 'src/game_file.dart' show GameFile; export 'src/game_version.dart' show GameVersion; export 'src/image.dart' show VgaImage; diff --git a/packages/wolf_3d_engine/lib/src/managers/door_manager.dart b/packages/wolf_3d_engine/lib/src/managers/door_manager.dart index 8ae20b6..193b96f 100644 --- a/packages/wolf_3d_engine/lib/src/managers/door_manager.dart +++ b/packages/wolf_3d_engine/lib/src/managers/door_manager.dart @@ -12,7 +12,7 @@ class DoorManager { DoorManager({required this.onPlaySound}); - void initDoors(Sprite wallGrid) { + void initDoors(SpriteMap wallGrid) { doors.clear(); for (int y = 0; y < wallGrid.length; y++) { for (int x = 0; x < wallGrid[y].length; x++) { diff --git a/packages/wolf_3d_engine/lib/src/managers/pushwall_manager.dart b/packages/wolf_3d_engine/lib/src/managers/pushwall_manager.dart index 2fbfc2b..f22756a 100644 --- a/packages/wolf_3d_engine/lib/src/managers/pushwall_manager.dart +++ b/packages/wolf_3d_engine/lib/src/managers/pushwall_manager.dart @@ -18,7 +18,7 @@ class PushwallManager { final Map pushwalls = {}; Pushwall? activePushwall; - void initPushwalls(Sprite wallGrid, Sprite objectGrid) { + void initPushwalls(SpriteMap wallGrid, SpriteMap objectGrid) { pushwalls.clear(); activePushwall = null; @@ -31,7 +31,7 @@ class PushwallManager { } } - void update(Duration elapsed, Sprite wallGrid) { + void update(Duration elapsed, SpriteMap wallGrid) { if (activePushwall == null) return; final pw = activePushwall!; @@ -84,7 +84,7 @@ class PushwallManager { double playerX, double playerY, double playerAngle, - Sprite wallGrid, + SpriteMap wallGrid, ) { // Only one pushwall can move at a time in the original engine! if (activePushwall != null) return; diff --git a/packages/wolf_3d_renderer/lib/raycast_painter.dart b/packages/wolf_3d_engine/lib/src/rasterizer.dart similarity index 53% rename from packages/wolf_3d_renderer/lib/raycast_painter.dart rename to packages/wolf_3d_engine/lib/src/rasterizer.dart index 0a427eb..ea018f2 100644 --- a/packages/wolf_3d_renderer/lib/raycast_painter.dart +++ b/packages/wolf_3d_engine/lib/src/rasterizer.dart @@ -1,65 +1,53 @@ 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 textures; - final Player player; - final double fov; - final Map doorOffsets; - final Pushwall? activePushwall; - final List sprites; - final List entities; +class SoftwareRasterizer { + // Pre-calculated VGA colors for ceiling and floor + final int ceilingColor = ColorPalette.vga32Bit[25]; + final int floorColor = ColorPalette.vga32Bit[29]; - RaycasterPainter({ - required this.map, - required this.textures, - required this.player, - required this.fov, - required this.doorOffsets, - this.activePushwall, - required this.sprites, - required this.entities, - }); + void render(WolfEngine engine, FrameBuffer buffer) { + // 1. Wipe the screen clean with ceiling and floor colors + _clearScreen(buffer); - @override - void paint(Canvas canvas, Size size) { - final Paint bgPaint = Paint()..isAntiAlias = false; + // 2. We need a Z-Buffer (1D array mapping to screen width) so sprites + // know if they are hiding behind a wall slice. + List zBuffer = List.filled(buffer.width, 0.0); - // 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]!, - ); + // 3. Do the math and draw the walls, filling the Z-Buffer as we go + _castWalls(engine, buffer, zBuffer); - const int renderWidth = 320; - double columnWidth = size.width / renderWidth; + // 4. Draw the entities/sprites + _castSprites(engine, buffer, zBuffer); + } - final Paint columnPaint = Paint() - ..isAntiAlias = false - ..strokeWidth = columnWidth + 0.5; + void _clearScreen(FrameBuffer buffer) { + int halfScreen = (buffer.width * buffer.height) ~/ 2; + buffer.pixels.fillRange(0, halfScreen, ceilingColor); + buffer.pixels.fillRange(halfScreen, buffer.pixels.length, floorColor); + } - List zBuffer = List.filled(renderWidth, 0.0); + void _castWalls(WolfEngine engine, FrameBuffer buffer, List zBuffer) { + final double fov = math.pi / 3; + final Player player = engine.player; + final SpriteMap map = engine.currentLevel; + + // Fetch dynamic states from the managers + final Map doorOffsets = engine.doorManager + .getOffsetsForRenderer(); + final Pushwall? activePushwall = engine.pushwallManager.activePushwall; 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; + for (int x = 0; x < buffer.width; x++) { + double cameraX = 2 * x / buffer.width - 1.0; Coordinate2D rayDir = dir + (plane * cameraX); int mapX = player.x.toInt(); @@ -78,8 +66,8 @@ class RaycasterPainter extends CustomPainter { int side = 0; int hitWallId = 0; - double textureOffset = 0.0; // Replaces doorOffset to handle both - bool customDistCalculated = false; // Flag to skip standard distance + double textureOffset = 0.0; + bool customDistCalculated = false; Set ignoredDoors = {}; if (rayDir.x < 0) { @@ -97,7 +85,7 @@ class RaycasterPainter extends CustomPainter { sideDistY = (mapY + 1.0 - player.y) * deltaDistY; } - // DDA Loop + // --- DDA LOOP --- while (!hit) { if (sideDistX < sideDistY) { sideDistX += deltaDistX; @@ -118,7 +106,7 @@ class RaycasterPainter extends CustomPainter { } else if (map[mapY][mapX] > 0) { String mapKey = '$mapX,$mapY'; - // --- DOOR LOGIC --- + // DOOR LOGIC if (map[mapY][mapX] >= 90 && !ignoredDoors.contains(mapKey)) { double currentOffset = doorOffsets[mapKey] ?? 0.0; if (currentOffset > 0.0) { @@ -131,37 +119,35 @@ class RaycasterPainter extends CustomPainter { wallXTemp -= wallXTemp.floor(); if (wallXTemp < currentOffset) { ignoredDoors.add(mapKey); - continue; // Ray passed through the open door gap + continue; } } hit = true; hitWallId = map[mapY][mapX]; textureOffset = currentOffset; } - // --- PUSHWALL LOGIC --- + // PUSHWALL LOGIC else if (activePushwall != null && - mapX == activePushwall!.x && - mapY == activePushwall!.y) { + mapX == activePushwall.x && + mapY == activePushwall.y) { hit = true; hitWallId = map[mapY][mapX]; - double pOffset = activePushwall!.offset; - int pDirX = activePushwall!.dirX; - int pDirY = activePushwall!.dirY; + 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 + perpWallDist = intersect; } else { - side = - 1; // Missed the front face, hit the newly exposed side! + side = 1; perpWallDist = sideDistY - deltaDistY; } } else { @@ -180,7 +166,6 @@ class RaycasterPainter extends CustomPainter { 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; @@ -189,23 +174,17 @@ class RaycasterPainter extends CustomPainter { 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 - } + if (hit) textureOffset = pOffset * pDirY; } 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) textureOffset = pOffset * pDirX; } } - if (!hit) continue; // The ray slipped past! Keep looping. - customDistCalculated = true; // Lock in our custom distance math + if (!hit) continue; + customDistCalculated = true; } - // --- STANDARD WALL --- + // STANDARD WALL else { hit = true; hitWallId = map[mapY][mapX]; @@ -215,7 +194,6 @@ class RaycasterPainter extends CustomPainter { 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); @@ -224,6 +202,7 @@ class RaycasterPainter extends CustomPainter { } } + // Log the distance so sprites know if they are occluded zBuffer[x] = perpWallDist; double wallX = (side == 0) @@ -231,32 +210,91 @@ class RaycasterPainter extends CustomPainter { : player.x + perpWallDist * rayDir.x; wallX -= wallX.floor(); - double drawX = x * columnWidth; - _drawTexturedColumn( - canvas, - drawX, + x, perpWallDist, wallX, side, - size, hitWallId, - textures, + engine.data.walls, // Wall textures textureOffset, - columnPaint, + buffer, + player.angle, ); } + } - // --- 2. DRAW SPRITES --- - // (Keep your existing sprite rendering logic exactly the same) - List activeSprites = List.from(entities); + void _drawTexturedColumn( + int x, + double distance, + double wallX, + int side, + int hitWallId, + List textures, + double textureOffset, + FrameBuffer buffer, + double playerAngle, + ) { + if (distance <= 0.01) distance = 0.01; + int lineHeight = (buffer.height / distance).toInt(); + + int drawStart = -lineHeight ~/ 2 + buffer.height ~/ 2; + if (drawStart < 0) drawStart = 0; + + int drawEnd = lineHeight ~/ 2 + buffer.height ~/ 2; + if (drawEnd >= buffer.height) drawEnd = buffer.height - 1; + + int texNum; + if (hitWallId >= 90) { + texNum = 98.clamp(0, textures.length - 1); + } else { + texNum = ((hitWallId - 1) * 2).clamp(0, textures.length - 2); + if (side == 1) texNum += 1; + } + + int texX = (((wallX - textureOffset) % 1.0) * 64).toInt().clamp(0, 63); + if (side == 0 && math.cos(playerAngle) > 0) texX = 63 - texX; + if (side == 1 && math.sin(playerAngle) < 0) texX = 63 - texX; + + double step = 64.0 / lineHeight; + double texPos = (drawStart - buffer.height / 2 + lineHeight / 2) * step; + + Sprite texture = textures[texNum]; + + for (int y = drawStart; y < drawEnd; y++) { + int texY = texPos.toInt() & 63; + texPos += step; + + int colorByte = texture.pixels[texX * 64 + texY]; + int color32 = ColorPalette.vga32Bit[colorByte]; + + buffer.pixels[y * buffer.width + x] = color32; + } + } + + void _castSprites( + WolfEngine engine, + FrameBuffer buffer, + List zBuffer, + ) { + final Player player = engine.player; + final List activeSprites = List.from(engine.entities); + + // Sort entities from furthest to closest (Painter's Algorithm) activeSprites.sort((a, b) { double distA = player.position.distanceTo(a.position); double distB = player.position.distanceTo(b.position); return distB.compareTo(distA); }); + Coordinate2D dir = Coordinate2D( + math.cos(player.angle), + math.sin(player.angle), + ); + Coordinate2D plane = + Coordinate2D(-dir.y, dir.x) * math.tan((math.pi / 3) / 2); + for (Entity entity in activeSprites) { Coordinate2D spritePos = entity.position - player.position; @@ -266,104 +304,55 @@ class RaycasterPainter extends CustomPainter { invDet * (-plane.y * spritePos.x + plane.x * spritePos.y); if (transformY > 0) { - int spriteScreenX = ((renderWidth / 2) * (1 + transformX / transformY)) + int spriteScreenX = ((buffer.width / 2) * (1 + transformX / transformY)) .toInt(); - int spriteHeight = (size.height / transformY).abs().toInt(); - int spriteColumnWidth = (spriteHeight / columnWidth).toInt(); + int spriteHeight = (buffer.height / transformY).abs().toInt(); - int drawStartX = -spriteColumnWidth ~/ 2 + spriteScreenX; - int drawEndX = spriteColumnWidth ~/ 2 + spriteScreenX; + // In 1x1 buffer pixels, the width of the sprite is equal to its height + int spriteWidth = spriteHeight; + + int drawStartY = -spriteHeight ~/ 2 + buffer.height ~/ 2; + if (drawStartY < 0) drawStartY = 0; + + int drawEndY = spriteHeight ~/ 2 + buffer.height ~/ 2; + if (drawEndY >= buffer.height) drawEndY = buffer.height - 1; + + int drawStartX = -spriteWidth ~/ 2 + spriteScreenX; + int drawEndX = spriteWidth ~/ 2 + spriteScreenX; int clipStartX = math.max(0, drawStartX); - int clipEndX = math.min(renderWidth - 1, drawEndX); + int clipEndX = math.min(buffer.width - 1, drawEndX); + + int safeIndex = entity.spriteIndex.clamp( + 0, + engine.data.sprites.length - 1, + ); + Sprite spritePixels = engine.data.sprites[safeIndex]; for (int stripe = clipStartX; stripe < clipEndX; stripe++) { + // Z-Buffer Check! Only draw the vertical stripe if it's in front of the wall if (transformY < zBuffer[stripe]) { - double texXDouble = (stripe - drawStartX) * 64 / spriteColumnWidth; - int texX = texXDouble.toInt().clamp(0, 63); + int texX = ((stripe - drawStartX) * 64 ~/ spriteWidth).clamp(0, 63); - double startY = (size.height / 2) - (spriteHeight / 2); - double stepY = spriteHeight / 64.0; - double drawX = stripe * columnWidth; + double step = 64.0 / spriteHeight; + double texPos = + (drawStartY - buffer.height / 2 + spriteHeight / 2) * step; - int safeIndex = entity.spriteIndex.clamp(0, sprites.length - 1); - Sprite spritePixels = sprites[safeIndex]; + for (int y = drawStartY; y < drawEndY; y++) { + int texY = texPos.toInt() & 63; + texPos += step; - for (int ty = 0; ty < 64; ty++) { - int colorByte = spritePixels[texX][ty]; + int colorByte = spritePixels.pixels[texX * 64 + texY]; 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, - ); - } + // 255 is transparent + buffer.pixels[y * buffer.width + stripe] = + ColorPalette.vga32Bit[colorByte]; } - startY += stepY; } } } } } } - - void _drawTexturedColumn( - Canvas canvas, - double drawX, - double distance, - double wallX, - int side, - Size size, - int hitWallId, - List 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; } diff --git a/packages/wolf_3d_engine/lib/src/wolf_3d_engine_base.dart b/packages/wolf_3d_engine/lib/src/wolf_3d_engine_base.dart index 4cca58d..7a158c2 100644 --- a/packages/wolf_3d_engine/lib/src/wolf_3d_engine_base.dart +++ b/packages/wolf_3d_engine/lib/src/wolf_3d_engine_base.dart @@ -31,7 +31,7 @@ class WolfEngine { // State late Player player; - late Level currentLevel; + late SpriteMap currentLevel; late WolfLevel activeLevel; List entities = []; @@ -93,7 +93,7 @@ class WolfEngine { activeLevel = episode.levels[_currentLevelIndex]; currentLevel = List.generate(64, (y) => List.from(activeLevel.wallGrid[y])); - final Level objectLevel = activeLevel.objectGrid; + final SpriteMap objectLevel = activeLevel.objectGrid; doorManager.initDoors(currentLevel); diff --git a/packages/wolf_3d_engine/lib/wolf_3d_engine.dart b/packages/wolf_3d_engine/lib/wolf_3d_engine.dart index 4a68f17..156deda 100644 --- a/packages/wolf_3d_engine/lib/wolf_3d_engine.dart +++ b/packages/wolf_3d_engine/lib/wolf_3d_engine.dart @@ -8,4 +8,5 @@ export 'src/engine_input.dart'; export 'src/managers/door_manager.dart'; export 'src/managers/pushwall_manager.dart'; export 'src/player/player.dart'; +export 'src/rasterizer.dart'; export 'src/wolf_3d_engine_base.dart'; diff --git a/packages/wolf_3d_renderer/lib/color_palette.dart b/packages/wolf_3d_renderer/lib/color_palette.dart deleted file mode 100644 index b504ceb..0000000 --- a/packages/wolf_3d_renderer/lib/color_palette.dart +++ /dev/null @@ -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 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 palette) { - return palette[findClosestIndex(palette)]; - } -} - -abstract class ColorPalette { - static const List 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), - ]; -} diff --git a/packages/wolf_3d_renderer/lib/weapon_painter.dart b/packages/wolf_3d_renderer/lib/weapon_painter.dart index 6bf0b92..bf7d66a 100644 --- a/packages/wolf_3d_renderer/lib/weapon_painter.dart +++ b/packages/wolf_3d_renderer/lib/weapon_painter.dart @@ -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( diff --git a/packages/wolf_3d_renderer/lib/wolf_3d_renderer.dart b/packages/wolf_3d_renderer/lib/wolf_3d_renderer.dart index 86d99e5..c4fc8b1 100644 --- a/packages/wolf_3d_renderer/lib/wolf_3d_renderer.dart +++ b/packages/wolf_3d_renderer/lib/wolf_3d_renderer.dart @@ -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 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 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 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 }, ), ), - - // --- UI --- Hud(player: engine.player), ], ), @@ -180,3 +189,34 @@ class _WolfRendererState extends State ); } } + +// --- 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; + } +}