diff --git a/lib/main.dart b/lib/main.dart index 324a58b..8629fd1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,19 +20,31 @@ const int mapX = 8; const int mapY = 8; const int mapSize = 32; +const double gameWidth = 320; +const double gameHeight = 256; + void main() { runApp(const MainApp()); } -class MainApp extends StatelessWidget { +class MainApp extends StatefulWidget { const MainApp({super.key}); + @override + State createState() => _MainAppState(); +} + +class _MainAppState extends State { + List rayData = []; + double playerAngle = 0; + Offset playerPosition = Offset.zero; + @override Widget build(BuildContext context) { return Directionality( textDirection: TextDirection.ltr, child: LayoutBuilder( - builder: (context, constraints) => const Row( + builder: (context, constraints) => Row( spacing: mapSize / 1, textDirection: TextDirection.ltr, children: [ @@ -42,14 +54,44 @@ class MainApp extends StatelessWidget { fit: BoxFit.contain, child: Stack( children: [ - Map(), - Player(), + Map( + rayData, + playerAngle: playerAngle, + playerPosition: playerPosition, + ), + Player( + onAngleUpdated: (angle) { + setState(() { + playerAngle = angle; + }); + }, + onPositionUpdated: (position) { + setState(() { + playerPosition = position; + }); + }, + onRaysUpdated: (rays) { + setState(() { + rayData = rays; + }); + }, + ), ], ), ), ), ), - Flexible(child: Background()), + Flexible( + child: Stack( + children: [ + const Background(), + Walls( + rayData, + playerAngle: playerAngle, + ), + ], + ), + ), ], ), ), @@ -59,7 +101,16 @@ class MainApp extends StatelessWidget { // * Player class Player extends StatefulWidget { - const new({super.key}); + const new({ + required this.onRaysUpdated, + required this.onAngleUpdated, + required this.onPositionUpdated, + super.key, + }); + + final ValueChanged> onRaysUpdated; + final ValueChanged onAngleUpdated; + final ValueChanged onPositionUpdated; @override State createState() => _PlayerState(); @@ -80,6 +131,12 @@ class _PlayerState extends State { void initState() { super.initState(); _focusNode = FocusNode()..requestFocus(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.onRaysUpdated(calculateRays(playerPosition, playerDelta)); + widget.onAngleUpdated(playerAngle); + widget.onPositionUpdated(playerPosition); + }); } @override @@ -88,120 +145,11 @@ class _PlayerState extends State { super.dispose(); } - @override - Widget build(BuildContext context) { - // TODO: Use HardwareKeyboard - return Focus( - focusNode: _focusNode, - onKeyEvent: (FocusNode node, value) { - if (value is! KeyDownEvent && value is! KeyRepeatEvent) { - return KeyEventResult.ignored; - } - - final LogicalKeyboardKey key = value.logicalKey; - bool handled = false; - - if (key == LogicalKeyboardKey.keyA) { - // Move left - playerAngle -= 0.1; - if (playerAngle < 0) { - playerAngle += 2 * math.pi; - } - handled = true; - } else if (key == LogicalKeyboardKey.keyD) { - // Move right - playerAngle += 0.1; - - if (playerAngle > 2 * math.pi) { - playerAngle -= 2 * math.pi; - } - handled = true; - } else if (key == LogicalKeyboardKey.keyW) { - // Move forward - playerPosition += playerDelta; - handled = true; - } else if (key == LogicalKeyboardKey.keyS) { - // Move backwwards - playerPosition -= playerDelta; - handled = true; - } - - if (handled) { - setState(() {}); - return KeyEventResult.handled; - } - - return KeyEventResult.ignored; - }, - child: Stack( - children: [ - CustomPaint( - painter: RayPainter( - playerAngle: playerAngle, - offset: playerPosition, - delta: playerDelta, - ), - ), - CustomPaint( - painter: PlayerPainter( - offset: playerPosition, - delta: playerDelta, - ), - ), - ], - ), - ); - } -} - -class PlayerPainter extends CustomPainter { - final Offset offset; - final Offset delta; - - const new({ - required this.offset, - required this.delta, - }); - - @override - void paint(Canvas canvas, Size size) { - // Player dot - canvas.drawRect( - Rect.fromLTWH(offset.dx, offset.dy, 8, 8), - Paint()..color = Colors.yellow, - ); - - // Player direction line - canvas.drawLine( - offset + const Offset(4, 4), - offset + const Offset(4, 4) + (delta * 5), - Paint() - ..color = Colors.yellow - ..strokeWidth = 2, - ); - } - - @override - bool shouldRepaint(covariant oldDelegate) => false; - @override - bool shouldRebuildSemantics(covariant oldDelegate) => false; -} - -class RayPainter extends CustomPainter { - final double playerAngle; - final Offset offset; - final Offset delta; - - const new({ - required this.playerAngle, - required this.offset, - required this.delta, - }); - - @override - void paint(Canvas canvas, Size size) { + List calculateRays(Offset offset, Offset delta) { const double dr = math.pi / 180; + final List rays = []; + int dof; int mx; int my; @@ -214,7 +162,7 @@ class RayPainter extends CustomPainter { double rayX = 0; double rayY = 0; - double disT; + double disT = 0; double rayAngle = delta.direction - (dr * 30); @@ -377,6 +325,8 @@ class RayPainter extends CustomPainter { if (mv == 2) { wallColor = const Color.fromRGBO(0, 179, 0, 1); } + + rays.add(RayData(Offset(rayX, rayY), disT, wallColor)); } if (distanceHor < distanceVert) { @@ -389,17 +339,11 @@ class RayPainter extends CustomPainter { if (mh == 2) { wallColor = const Color.fromRGBO(0, 102, 0, 1); } + + rays.add(RayData(Offset(rayX, rayY), disT, wallColor)); } - canvas.drawLine( - playerCenter, - Offset(rayX, rayY), - Paint() - ..color = wallColor - ..isAntiAlias = false - ..strokeWidth = 1.5, - ); - + // * Increase the ray angle to draw the next ray rayAngle += dr; if (rayAngle < 0) { @@ -409,12 +353,187 @@ class RayPainter extends CustomPainter { rayAngle -= 2 * math.pi; } } + + return rays; } double dist(double ax, double ay, double bx, double by, double ang) { return math.sqrt((bx - ax) * (bx - ax) + (by - ay) * (by - ay)); } + @override + Widget build(BuildContext context) { + // TODO: Use HardwareKeyboard + return Focus( + focusNode: _focusNode, + onKeyEvent: (FocusNode node, value) { + if (value is! KeyDownEvent && value is! KeyRepeatEvent) { + return KeyEventResult.ignored; + } + + final LogicalKeyboardKey key = value.logicalKey; + bool handled = false; + + if (key == LogicalKeyboardKey.keyA) { + // Move left + playerAngle -= 0.1; + if (playerAngle < 0) { + playerAngle += 2 * math.pi; + } + handled = true; + } else if (key == LogicalKeyboardKey.keyD) { + // Move right + playerAngle += 0.1; + + if (playerAngle > 2 * math.pi) { + playerAngle -= 2 * math.pi; + } + handled = true; + } else if (key == LogicalKeyboardKey.keyW) { + // Move forward + playerPosition += playerDelta; + handled = true; + } else if (key == LogicalKeyboardKey.keyS) { + // Move backwwards + playerPosition -= playerDelta; + handled = true; + } + + if (handled) { + widget.onRaysUpdated(calculateRays(playerPosition, playerDelta)); + widget.onAngleUpdated(playerAngle); + widget.onPositionUpdated(playerPosition); + return KeyEventResult.handled; + } + + return KeyEventResult.ignored; + }, + child: CustomPaint( + painter: PlayerPainter( + offset: playerPosition, + delta: playerDelta, + ), + ), + ); + } +} + +class PlayerPainter extends CustomPainter { + final Offset offset; + final Offset delta; + + const new({ + required this.offset, + required this.delta, + }); + + @override + void paint(Canvas canvas, Size size) { + // Player dot + canvas.drawRect( + Rect.fromLTWH(offset.dx, offset.dy, 8, 8), + Paint()..color = Colors.yellow, + ); + + // Player direction line + canvas.drawLine( + offset + const Offset(4, 4), + offset + const Offset(4, 4) + (delta * 5), + Paint() + ..color = Colors.yellow + ..strokeWidth = 2, + ); + } + + @override + bool shouldRepaint(covariant oldDelegate) => false; + @override + bool shouldRebuildSemantics(covariant oldDelegate) => false; +} + +// * Walls + +class Walls extends StatelessWidget { + const new(this.rayData, {required this.playerAngle, super.key}); + + final List rayData; + final double playerAngle; + + @override + Widget build(BuildContext context) { + return Center( + child: FittedBox( + fit: BoxFit.cover, + child: SizedBox( + width: gameWidth, + height: gameHeight, + child: CustomPaint( + painter: WallPainter( + playerAngle: playerAngle, + rayData: rayData, + ), + ), + ), + ), + ); + } +} + +class WallPainter extends CustomPainter { + final double playerAngle; + final List rayData; + + const new({ + required this.playerAngle, + + required this.rayData, + }); + + @override + void paint(Canvas canvas, Size size) { + for (final RayData ray in rayData) { + double disT = ray.distance; + + // * --- Draw 3D Walls --- + // Correct the fish-eye effect + const double dr = math.pi / 180; + final double rayAngle = + playerAngle - (dr * 30) + (rayData.indexOf(ray) * dr); + + double correctedAngle = playerAngle - rayAngle; + if (correctedAngle < 0) { + correctedAngle += 2 * math.pi; + } + if (correctedAngle > 2 * math.pi) { + correctedAngle -= 2 * math.pi; + } + + disT = disT * math.cos(correctedAngle); + + double lineHeight = (mapSize * gameHeight) / disT; + if (lineHeight > gameHeight) { + lineHeight = gameHeight; + } + + if (lineHeight > gameWidth) { + lineHeight = gameWidth; + } + + final double lineOffset = (gameHeight / 2.0) - (lineHeight / 2.0); + + final double rayWidth = gameWidth / rayData.length; + final double rayX3D = rayData.indexOf(ray) * rayWidth; + + canvas.drawRect( + Rect.fromLTWH(rayX3D, lineOffset, rayWidth, lineHeight), + Paint() + ..color = ray.color + ..isAntiAlias = false + ..strokeWidth = 1.5, + ); + } + } + @override bool shouldRepaint(covariant oldDelegate) => false; @override @@ -423,21 +542,42 @@ class RayPainter extends CustomPainter { // * Map class Map extends StatelessWidget { - const new({super.key}); + const new( + this.rayData, { + required this.playerAngle, + required this.playerPosition, + super.key, + }); + + final List rayData; + final double playerAngle; + final Offset playerPosition; @override Widget build(BuildContext context) { const double width = mapX * mapSize / 1; const double height = mapY * mapSize / 1; - return const CustomPaint( - size: Size(width, height), - painter: MapPainter( - mapData: map, - mapX: mapX, - mapY: mapY, - tileSize: mapSize / 1, - ), + return Stack( + children: [ + const CustomPaint( + size: Size(width, height), + painter: MapPainter( + mapData: map, + mapX: mapX, + mapY: mapY, + tileSize: mapSize / 1, + ), + ), + + CustomPaint( + painter: MapRayPainter( + playerAngle: playerAngle, + origin: playerPosition, + rayData: rayData, + ), + ), + ], ); } } @@ -483,6 +623,39 @@ class MapPainter extends CustomPainter { bool shouldRebuildSemantics(covariant oldDelegate) => false; } +class MapRayPainter extends CustomPainter { + final double playerAngle; + final Offset origin; + final List rayData; + + const new({ + required this.playerAngle, + required this.origin, + required this.rayData, + }); + + @override + void paint(Canvas canvas, Size size) { + final Offset playerCenter = origin + const Offset(4, 4); + + for (final RayData ray in rayData) { + canvas.drawLine( + playerCenter, + ray.hitPosition, + Paint() + ..color = ray.color + ..isAntiAlias = false + ..strokeWidth = 1.5, + ); + } + } + + @override + bool shouldRepaint(covariant oldDelegate) => false; + @override + bool shouldRebuildSemantics(covariant oldDelegate) => false; +} + // * Background class Background extends StatelessWidget { const new({ @@ -574,3 +747,16 @@ class Floor extends CustomPainter { @override bool shouldRebuildSemantics(covariant oldDelegate) => false; } + +// * Ray Data + +class RayData( + /// Where it hit on the 2D map + var Offset hitPosition, + + /// Corrected 3D distance + var double distance, + + /// Wall color + var Color color, +);