From eaa6d76792018251d3c47c9f227514175f28a426 Mon Sep 17 00:00:00 2001 From: Hans Kokx Date: Thu, 10 Sep 2026 20:25:52 +0200 Subject: [PATCH] First pass at Flutter collision checks Signed-off-by: Hans Kokx --- lib/main.dart | 233 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 165 insertions(+), 68 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index d1325ae..10fac48 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -23,6 +23,58 @@ const int mapSize = 32; const double gameWidth = 320; const double gameHeight = 256; +extension GameValues on double { + /// Normalizes an angle back into [0, 2π]. + double get normalize => (this % (2 * math.pi) + 2 * math.pi) % (2 * math.pi); + + /// Returns the degree angle + double get radianToDegree { + final double proposed = this * 180 / math.pi; + + if (proposed > 360) { + throw Exception( + "Proposed angle is $proposed, which is larger than 360. Check input: $this", + ); + } + + if (proposed.isNegative) { + throw Exception( + "Proposed angle is $proposed, which is negative. Check input: $this", + ); + } + + return proposed; + } + + // Calculate the direction we're facing using the hypotenuse of the angle. + // This means the x is positive when we're facing "east" and negative when facing "west" + // and y is positive when facting "north" while negative when facing "south". + // Flutter's coordinate system flips the y-axis, which is why we have -sin() -- to flip + // it back in the correct direction. + Offset get direction => Offset( + math.cos(normalize), + -math.sin(normalize), + ); + + /// Get the delta from the given angle + Offset get playerDelta => Offset( + math.cos(this) * Player.speed, + math.sin(this) * Player.speed, + ); +} + +extension WorldValues on Offset { + // The current "tile" we're in on the map + Offset get currentTile => Offset( + (dx / mapSize).floorToDouble(), + (dy / mapSize).floorToDouble(), + ); + + // The index of the current "tile" we're in + // index = row * stride + column + int get index => (currentTile.dy.toInt() * mapX) + currentTile.dx.toInt(); +} + void main() { runApp(const MainApp()); } @@ -37,7 +89,7 @@ class MainApp extends StatefulWidget { class _MainAppState extends State { RayData rayData = const RayData( playerAngle: 0, - playerPosition: Offset.zero, + playerPosition: Offset(1.5 * mapSize, 1.5 * mapSize), rays: [], ); @@ -60,10 +112,12 @@ class _MainAppState extends State { children: [ Map(rayData), Player( - onAngleUpdated: (angle) { + rayData: rayData, + onAngleUpdated: (radians) { + // degrees = radians × (180 / π) setState(() { rayData = rayData.copyWith( - playerAngle: angle, + playerAngle: radians, ); }); }, @@ -110,44 +164,133 @@ class Player extends StatefulWidget { required this.onRaysUpdated, required this.onAngleUpdated, required this.onPositionUpdated, + required this.rayData, super.key, }); final ValueChanged> onRaysUpdated; final ValueChanged onAngleUpdated; final ValueChanged onPositionUpdated; + final RayData rayData; + + /// How "fast" the player moves + static const int speed = 5; @override State createState() => _PlayerState(); } class _PlayerState extends State { - late final FocusNode _focusNode; - - double playerAngle = 0; - double get playerDeltaX => math.cos(playerAngle) * 5; - double get playerDeltaY => math.sin(playerAngle) * 5; - - Offset get playerDelta => Offset(playerDeltaX, playerDeltaY); - - Offset playerPosition = const Offset(1.5 * mapSize, 1.5 * mapSize); - @override void initState() { super.initState(); - _focusNode = FocusNode()..requestFocus(); + + HardwareKeyboard.instance.addHandler(keyPressHandler); WidgetsBinding.instance.addPostFrameCallback((_) { - widget.onRaysUpdated(calculateRays(playerPosition, playerDelta)); - widget.onAngleUpdated(playerAngle); - widget.onPositionUpdated(playerPosition); + widget.onRaysUpdated( + calculateRays( + widget.rayData.playerPosition, + widget.rayData.playerAngle.playerDelta, + ), + ); + + widget.onAngleUpdated( + widget.rayData.playerAngle, + ); + + widget.onPositionUpdated( + widget.rayData.playerPosition, + ); }); } @override void dispose() { - _focusNode.dispose(); super.dispose(); + HardwareKeyboard.instance.removeHandler(keyPressHandler); + } + + /// Get the map tile at a given pixel coordinate + int tileAt(Offset position) { + final int col = (position.dx / mapSize).floor(); + final int row = (position.dy / mapSize).floor(); + + if (col < 0 || col >= mapX || row < 0 || row >= mapY) return 1; // solid + + return map[row * mapX + col]; + } + + /// How far until we hit a wall in the movement direction? + double safeDistanceToWall(Offset position, Offset direction) { + const int maxCheck = 64; + const int stepSize = 8; + + for (double d = 0; d < maxCheck; d += stepSize) { + final Offset p = position + Offset(direction.dx, direction.dy) * d; + if (tileAt(p) > 0) return d; + } + + return double.infinity; + } + + bool keyPressHandler(KeyEvent event) { + final Set keys = + HardwareKeyboard.instance.logicalKeysPressed; + + bool handled = false; + + // Where is the player now? + Offset playerPosition = widget.rayData.playerPosition; + double playerAngle = widget.rayData.playerAngle; + final Offset direction = playerAngle.direction; + + // How many pixels away from a wall to stay + const double buffer = 8; + + if (keys.contains(LogicalKeyboardKey.keyA)) { + // Move left + playerAngle -= 0.1; + if (playerAngle < 0) { + playerAngle += 2 * math.pi; + } + handled = true; + } else if (keys.contains(LogicalKeyboardKey.keyD)) { + // Move right + playerAngle += 0.1; + + if (playerAngle > 2 * math.pi) { + playerAngle -= 2 * math.pi; + } + handled = true; + } + + if (keys.contains(LogicalKeyboardKey.keyW)) { + if (safeDistanceToWall(playerPosition, direction) > + Player.speed + buffer) { + // Move forward + playerPosition += playerAngle.playerDelta; + } + handled = true; + } else if (keys.contains(LogicalKeyboardKey.keyS)) { + if (safeDistanceToWall(playerPosition, -direction) > + Player.speed + buffer) { + // Move backwwards + playerPosition -= playerAngle.playerDelta; + } + handled = true; + } + + if (handled) { + widget.onRaysUpdated( + calculateRays(playerPosition, playerAngle.playerDelta), + ); + widget.onAngleUpdated(playerAngle); + widget.onPositionUpdated(playerPosition); + return true; + } + + return false; } List calculateRays(Offset offset, Offset delta) { @@ -368,56 +511,10 @@ class _PlayerState extends State { @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, - ), + return CustomPaint( + painter: PlayerPainter( + offset: widget.rayData.playerPosition, + delta: widget.rayData.playerAngle.playerDelta, ), ); }