diff --git a/lib/features/entities/entity_registry.dart b/lib/features/entities/entity_registry.dart index 329662a..94f8c56 100644 --- a/lib/features/entities/entity_registry.dart +++ b/lib/features/entities/entity_registry.dart @@ -5,7 +5,12 @@ import 'package:wolf_dart/features/entities/enemies/dog.dart'; import 'package:wolf_dart/features/entities/entity.dart'; typedef EntitySpawner = - Entity? Function(int objId, double x, double y, int difficultyLevel); + Entity? Function( + int objId, + double x, + double y, + int difficultyLevel, + ); abstract class EntityRegistry { // Add future enemies (SSGuard, Dog, etc.) to this list! diff --git a/lib/features/renderer/raycast_painter.dart b/lib/features/renderer/raycast_painter.dart index 24d6907..fe1ec9c 100644 --- a/lib/features/renderer/raycast_painter.dart +++ b/lib/features/renderer/raycast_painter.dart @@ -37,10 +37,18 @@ class RaycasterPainter extends CustomPainter { Paint()..color = Colors.brown[900]!, ); - int screenWidth = size.width.toInt(); + // --- OPTIMIZATION: Lock to Retro Resolution --- + const int renderWidth = 320; - // The 1D Z-Buffer - List zBuffer = List.filled(screenWidth, 0.0); + // Calculate how wide each column should be on the actual screen + double columnWidth = size.width / renderWidth; + + // Create a single Paint object to reuse (massive performance boost) + // Add 0.5 to strokeWidth to prevent anti-aliasing seams between columns + final Paint columnPaint = Paint()..strokeWidth = columnWidth + 0.5; + + // The 1D Z-Buffer locked to our render width + List zBuffer = List.filled(renderWidth, 0.0); double dirX = math.cos(player.angle); double dirY = math.sin(player.angle); @@ -48,8 +56,8 @@ class RaycasterPainter extends CustomPainter { double planeY = dirX * math.tan(fov / 2); // --- 1. CAST WALLS --- - for (int x = 0; x < screenWidth; x++) { - double cameraX = 2 * x / screenWidth - 1.0; + for (int x = 0; x < renderWidth; x++) { + double cameraX = 2 * x / renderWidth - 1.0; double rayDirX = dirX + planeX * cameraX; double rayDirY = dirY + planeY * cameraX; @@ -135,7 +143,6 @@ class RaycasterPainter extends CustomPainter { perpWallDist = (sideDistY - deltaDistY); } - // STORE THE DISTANCE IN THE Z-BUFFER! zBuffer[x] = perpWallDist; double wallX; @@ -146,9 +153,12 @@ class RaycasterPainter extends CustomPainter { } wallX -= wallX.floor(); + // Pass the scaled drawX instead of the raw loop index + double drawX = x * columnWidth; + _drawTexturedColumn( canvas, - x, + drawX, // <-- Updated perpWallDist, wallX, side, @@ -156,12 +166,11 @@ class RaycasterPainter extends CustomPainter { hitWallId, textures, doorOffset, + columnPaint, // <-- Pass the reusable Paint object ); } // --- 2. DRAW SPRITES --- - - // Sort sprites from furthest to closest (Painter's Algorithm) List activeSprites = List.from(entities); activeSprites.sort((a, b) { double distA = @@ -172,64 +181,58 @@ class RaycasterPainter extends CustomPainter { }); for (Entity entity in activeSprites) { - // Translate sprite position to relative to camera double spriteX = entity.x - player.x; double spriteY = entity.y - player.y; - // Inverse camera matrix (Transform to screen space) double invDet = 1.0 / (planeX * dirY - dirX * planeY); double transformX = invDet * (dirY * spriteX - dirX * spriteY); double transformY = invDet * (-planeY * spriteX + planeX * spriteY); - // print("Sprite at ${sprite.x}, ${sprite.y} transformY: $transformY"); - - // Is the sprite in front of the camera? if (transformY > 0) { - // print( - // "Attempting to draw Sprite Index: ${sprite.spriteIndex} (Total Sprites Loaded: ${sprites.length})", - // ); - - int spriteScreenX = ((screenWidth / 2) * (1 + transformX / transformY)) + // Map sprite X to our 320 renderWidth + int spriteScreenX = ((renderWidth / 2) * (1 + transformX / transformY)) .toInt(); - // Calculate height and width (Sprites are 64x64 squares) + // Calculate height in REAL screen pixels int spriteHeight = (size.height / transformY).abs().toInt(); - int spriteWidth = spriteHeight; - int drawStartX = -spriteWidth ~/ 2 + spriteScreenX; - int drawEndX = spriteWidth ~/ 2 + spriteScreenX; + // Calculate width in COLUMNS (320 space) to maintain the square aspect ratio + int spriteColumnWidth = (spriteHeight / columnWidth).toInt(); + + // Use the new column width for start/end points + int drawStartX = -spriteColumnWidth ~/ 2 + spriteScreenX; + int drawEndX = spriteColumnWidth ~/ 2 + spriteScreenX; // Clip to screen boundaries int clipStartX = math.max(0, drawStartX); - int clipEndX = math.min(screenWidth - 1, drawEndX); + int clipEndX = math.min(renderWidth - 1, drawEndX); for (int stripe = clipStartX; stripe < clipEndX; stripe++) { // THE Z-BUFFER CHECK! - // Only draw if the sprite is closer to the camera than the wall at this pixel column if (transformY < zBuffer[stripe]) { - double texXDouble = (stripe - drawStartX) * 64 / spriteWidth; + // Map the texture X using the new column width! + 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; - // Safeguard against bad sprite indices + double drawX = stripe * columnWidth; + int safeIndex = entity.spriteIndex.clamp(0, sprites.length - 1); Matrix spritePixels = sprites[safeIndex]; for (int ty = 0; ty < 64; ty++) { int colorByte = spritePixels[texX][ty]; - // Only draw if the pixel is NOT 255 (Magenta Transparency) if (colorByte != 255) { double endY = startY + stepY; if (endY > 0 && startY < size.height) { + columnPaint.color = ColorPalette.vga[colorByte]; canvas.drawLine( - Offset(stripe.toDouble(), startY), - Offset(stripe.toDouble(), endY), - Paint() - ..color = ColorPalette.vga[colorByte] - ..strokeWidth = 1.1, + Offset(drawX, startY), + Offset(drawX, endY), + columnPaint, ); } } @@ -243,7 +246,7 @@ class RaycasterPainter extends CustomPainter { void _drawTexturedColumn( Canvas canvas, - int x, + double drawX, // <-- Receive scaled draw position double distance, double wallX, int side, @@ -251,24 +254,20 @@ class RaycasterPainter extends CustomPainter { int hitWallId, List> textures, double doorOffset, + Paint paint, // <-- Receive reused Paint object ) { if (distance <= 0.01) distance = 0.01; double wallHeight = size.height / distance; int drawStart = ((size.height / 2) - (wallHeight / 2)).toInt(); - // TEXTURE MAPPING - // Wolf3D stores textures in pairs. Even = N/S (Light), Odd = E/W (Dark). int texNum = ((hitWallId - 1) * 2).clamp(0, textures.length - 2); int texX = (wallX * 64).toInt().clamp(0, 63); - // INTERCEPT DOORS if (hitWallId >= 90) { - // Safely clamp the door texture index so it never crashes the paint loop! texNum = 98.clamp(0, textures.length - 1); texX = ((wallX - doorOffset) * 64).toInt().clamp(0, 63); } else { - // Standard wall texture pairing texNum = ((hitWallId - 1) * 2).clamp(0, textures.length - 2); if (side == 1) texNum += 1; } @@ -282,19 +281,16 @@ class RaycasterPainter extends CustomPainter { for (int ty = 0; ty < 64; ty++) { int colorByte = textures[texNum][texX][ty]; - // 2. NO MORE SHADOW MATH - // Because we selected the correct dark texture above, we just draw the raw color! - Color pixelColor = ColorPalette.vga[colorByte]; + // Update the color of our shared paint object + paint.color = ColorPalette.vga[colorByte]; double endY = startY + stepY; if (endY > 0 && startY < size.height) { canvas.drawLine( - Offset(x.toDouble(), startY), - Offset(x.toDouble(), endY), - Paint() - ..color = pixelColor - ..strokeWidth = 1.1, + Offset(drawX, startY), + Offset(drawX, endY), + paint, ); } @@ -303,7 +299,7 @@ class RaycasterPainter extends CustomPainter { } @override - bool shouldRepaint(covariant RaycasterPainter oldDelegate) { + bool shouldRepaint(RaycasterPainter oldDelegate) { return oldDelegate.player != player || oldDelegate.player.angle != player.angle; } diff --git a/lib/features/renderer/renderer.dart b/lib/features/renderer/renderer.dart index 2b712ca..9980d03 100644 --- a/lib/features/renderer/renderer.dart +++ b/lib/features/renderer/renderer.dart @@ -414,63 +414,71 @@ class _WolfRendererState extends State Expanded( child: LayoutBuilder( builder: (context, constraints) { - return Stack( - children: [ - CustomPaint( - size: Size(constraints.maxWidth, constraints.maxHeight), - painter: RaycasterPainter( - map: currentLevel, - textures: gameMap.textures, - player: player, - fov: fov, - doorOffsets: doorOffsets, - entities: entities, - sprites: gameMap.sprites, - ), - ), - // Weapon Viewmodel - Positioned( - bottom: -20, - left: 0, - right: 0, - child: Center( - child: Transform.translate( - offset: Offset( - 0, - // Bobbing math: only moves if velocity is > 0 - (moveStepX.abs() + moveStepY.abs()) > 0 - ? math.sin( - DateTime.now() - .millisecondsSinceEpoch / - 100, - ) * - 12 - : 0, + return Center( + child: AspectRatio( + aspectRatio: 16 / 10, + child: Stack( + children: [ + CustomPaint( + size: Size( + constraints.maxWidth, + constraints.maxHeight, ), - child: SizedBox( - width: 500, - height: 500, - child: CustomPaint( - painter: WeaponPainter( - sprite: - gameMap.sprites[player - .currentWeapon - .currentSprite], + painter: RaycasterPainter( + map: currentLevel, + textures: gameMap.textures, + player: player, + fov: fov, + doorOffsets: doorOffsets, + entities: entities, + sprites: gameMap.sprites, + ), + ), + // Weapon Viewmodel + Positioned( + bottom: -20, + left: 0, + right: 0, + child: Center( + child: Transform.translate( + offset: Offset( + 0, + // Bobbing math: only moves if velocity is > 0 + (moveStepX.abs() + moveStepY.abs()) > 0 + ? math.sin( + DateTime.now() + .millisecondsSinceEpoch / + 100, + ) * + 12 + : 0, + ), + child: SizedBox( + width: 500, + height: 500, + child: CustomPaint( + painter: WeaponPainter( + sprite: + gameMap.sprites[player + .currentWeapon + .currentSprite], + ), + ), ), ), ), ), - ), - ), - if (damageFlashOpacity > 0) - Positioned.fill( - child: Container( - color: Colors.red.withValues( - alpha: damageFlashOpacity, + if (damageFlashOpacity > 0) + Positioned.fill( + child: Container( + color: Colors.red.withValues( + alpha: damageFlashOpacity, + ), + ), ), - ), - ), - ], + ], + ), + ), ); }, ),