import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; typedef Matrix = List>; void main() => runApp(const MaterialApp(home: WolfRenderer())); typedef LinearCoordinates = ({double x, double y}); class WolfRenderer extends StatefulWidget { const WolfRenderer({super.key}); @override State createState() => _WolfRendererState(); } class _WolfRendererState extends State with SingleTickerProviderStateMixin { late Ticker _gameLoop; final FocusNode _focusNode = FocusNode(); final Matrix map = [ [1, 1, 1, 1, 1, 1, 1, 1], [1, 0, 0, 0, 0, 0, 0, 1], [1, 0, 1, 0, 0, 1, 0, 1], [1, 0, 1, 0, 0, 1, 0, 1], [1, 0, 0, 0, 0, 0, 0, 1], [1, 1, 1, 1, 1, 1, 1, 1], ]; LinearCoordinates player = (x: 2.5, y: 2.5); double playerAngle = 0.0; final double fov = math.pi / 3; @override void initState() { super.initState(); _bumpPlayerIfStuck(); _gameLoop = createTicker(_tick)..start(); _focusNode.requestFocus(); } @override void dispose() { _gameLoop.dispose(); _focusNode.dispose(); super.dispose(); } void _bumpPlayerIfStuck() { int pX = player.x.toInt(); int pY = player.y.toInt(); if (pY < 0 || pY >= map.length || pX < 0 || pX >= map[0].length || map[pY][pX] > 0) { double shortestDist = double.infinity; LinearCoordinates nearestSafeSpot = (x: 1.5, y: 1.5); for (int y = 0; y < map.length; y++) { for (int x = 0; x < map[y].length; x++) { if (map[y][x] == 0) { double safeX = x + 0.5; double safeY = y + 0.5; double dist = math.sqrt( math.pow(safeX - player.x, 2) + math.pow(safeY - player.y, 2), ); if (dist < shortestDist) { shortestDist = dist; nearestSafeSpot = (x: safeX, y: safeY); } } } } player = nearestSafeSpot; } } // Updated Movement Logic void _tick(Duration elapsed) { const double moveSpeed = 0.05; const double turnSpeed = 0.04; // Added a dedicated turning speed double newX = player.x; double newY = player.y; final pressedKeys = HardwareKeyboard.instance.logicalKeysPressed; if (pressedKeys.contains(LogicalKeyboardKey.keyW)) { newX += math.cos(playerAngle) * moveSpeed; newY += math.sin(playerAngle) * moveSpeed; } if (pressedKeys.contains(LogicalKeyboardKey.keyS)) { newX -= math.cos(playerAngle) * moveSpeed; newY -= math.sin(playerAngle) * moveSpeed; } if (pressedKeys.contains(LogicalKeyboardKey.keyA)) { playerAngle -= turnSpeed; } if (pressedKeys.contains(LogicalKeyboardKey.keyD)) { playerAngle += turnSpeed; } // Keep the angle mapped cleanly between 0 and 2*PI (optional, but good practice) if (playerAngle < 0) playerAngle += 2 * math.pi; if (playerAngle > 2 * math.pi) playerAngle -= 2 * math.pi; if (map[newY.toInt()][newX.toInt()] == 0) { player = (x: newX, y: newY); } setState(() {}); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, body: KeyboardListener( focusNode: _focusNode, autofocus: true, onKeyEvent: (_) {}, child: LayoutBuilder( builder: (context, constraints) { return CustomPaint( size: Size(constraints.maxWidth, constraints.maxHeight), painter: RaycasterPainter( map: map, player: player, playerAngle: playerAngle, fov: fov, ), ); }, ), ), ); } } class RaycasterPainter extends CustomPainter { final Matrix map; final LinearCoordinates player; final double playerAngle; final double fov; RaycasterPainter({ required this.map, required this.player, required this.playerAngle, required this.fov, }); @override void paint(Canvas canvas, Size size) { // 1. Draw Ceiling & Floor canvas.drawRect( Rect.fromLTWH(0, 0, size.width, size.height / 2), Paint()..color = Colors.blueGrey[900]!, ); canvas.drawRect( Rect.fromLTWH(0, size.height / 2, size.width, size.height / 2), Paint()..color = Colors.brown[900]!, ); int screenWidth = size.width.toInt(); // 2. Camera Plane Setup // Direction vector of the player double dirX = math.cos(playerAngle); double dirY = math.sin(playerAngle); // The camera plane is perpendicular to the direction vector. // Multiplying by tan(fov/2) scales the plane to match our field of view. double planeX = -dirY * math.tan(fov / 2); double planeY = dirX * math.tan(fov / 2); for (int x = 0; x < screenWidth; x++) { // Calculate where on the camera plane this ray passes (-1 is left edge, 1 is right edge) double cameraX = 2 * x / screenWidth - 1.0; double rayDirX = dirX + planeX * cameraX; double rayDirY = dirY + planeY * cameraX; // Current map box we are in int mapX = player.x.toInt(); int mapY = player.y.toInt(); // Length of ray from current position to next x or y-side double sideDistX; double sideDistY; // Length of ray from one x or y-side to next x or y-side double deltaDistX = (rayDirX == 0) ? double.infinity : (1.0 / rayDirX).abs(); double deltaDistY = (rayDirY == 0) ? double.infinity : (1.0 / rayDirY).abs(); double perpWallDist; // Direction to step in x or y direction (+1 or -1) int stepX; int stepY; bool hit = false; int side = 0; // 0 for North/South (vertical) walls, 1 for East/West (horizontal) walls // Calculate step and initial sideDist if (rayDirX < 0) { stepX = -1; sideDistX = (player.x - mapX) * deltaDistX; } else { stepX = 1; sideDistX = (mapX + 1.0 - player.x) * deltaDistX; } if (rayDirY < 0) { stepY = -1; sideDistY = (player.y - mapY) * deltaDistY; } else { stepY = 1; sideDistY = (mapY + 1.0 - player.y) * deltaDistY; } // 3. The True DDA Loop while (!hit) { // Jump to next map square, either in x-direction, or in y-direction if (sideDistX < sideDistY) { sideDistX += deltaDistX; mapX += stepX; side = 0; } else { sideDistY += deltaDistY; mapY += stepY; side = 1; } // Check bounds and wall collisions if (mapY < 0 || mapY >= map.length || mapX < 0 || mapX >= map[0].length) { hit = true; perpWallDist = 20.0; // Out of bounds fallback } else if (map[mapY][mapX] > 0) { hit = true; } } // Calculate distance projected on camera direction (No fisheye effect!) if (side == 0) { perpWallDist = (sideDistX - deltaDistX); } else { perpWallDist = (sideDistY - deltaDistY); } // 4. Calculate exact wall hit coordinate for textures double wallX; if (side == 0) { wallX = player.y + perpWallDist * rayDirY; } else { wallX = player.x + perpWallDist * rayDirX; } wallX -= wallX.floor(); // Get just the fractional part (0.0 to 0.99) _drawTexturedColumn(canvas, x, perpWallDist, wallX, side, size); } } void _drawTexturedColumn( Canvas canvas, int x, double distance, double wallX, int side, Size size, ) { if (distance <= 0.01) distance = 0.01; double wallHeight = size.height / distance; double drawStart = (size.height / 2) - (wallHeight / 2); double drawEnd = (size.height / 2) + (wallHeight / 2); // --- PROCEDURAL TEXTURE LOGIC --- Color baseColor; // Draw a dark edge on the sides of the block to create "tiles" if (wallX < 0.05 || wallX > 0.95) { baseColor = Colors.black87; } else { baseColor = Colors.teal; // Main wall color } // Faux-Lighting: Darken East/West walls to give a 3D pop to corners if (side == 1) { baseColor = Color.fromARGB( 255, ((baseColor.r * 255).round().clamp(0, 255) * 0.7).toInt(), ((baseColor.g * 255).round().clamp(0, 255) * 0.7).toInt(), ((baseColor.b * 255).round().clamp(0, 255) * 0.7).toInt(), ); } // Depth cueing: Dim colors as they get further away double dimFactor = (1.0 - (distance / 15)).clamp(0.0, 1.0); Color finalColor = Color.fromARGB( 255, ((baseColor.r * 255).round().clamp(0, 255) * dimFactor).toInt(), ((baseColor.g * 255).round().clamp(0, 255) * dimFactor).toInt(), ((baseColor.b * 255).round().clamp(0, 255) * dimFactor).toInt(), ); final paint = Paint() ..color = finalColor ..strokeWidth = 1.1 // Prevent transparent gaps between line strokes ..style = PaintingStyle.stroke; canvas.drawLine( Offset(x.toDouble(), drawStart), Offset(x.toDouble(), drawEnd), paint, ); } @override bool shouldRepaint(covariant RaycasterPainter oldDelegate) { return oldDelegate.player != player || oldDelegate.playerAngle != playerAngle; } }