First pass at Flutter collision checks

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-09-10 20:25:52 +02:00
parent 91fdd219b6
commit eaa6d76792
+164 -67
View File
@@ -23,6 +23,58 @@ const int mapSize = 32;
const double gameWidth = 320; const double gameWidth = 320;
const double gameHeight = 256; 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() { void main() {
runApp(const MainApp()); runApp(const MainApp());
} }
@@ -37,7 +89,7 @@ class MainApp extends StatefulWidget {
class _MainAppState extends State<MainApp> { class _MainAppState extends State<MainApp> {
RayData rayData = const RayData( RayData rayData = const RayData(
playerAngle: 0, playerAngle: 0,
playerPosition: Offset.zero, playerPosition: Offset(1.5 * mapSize, 1.5 * mapSize),
rays: [], rays: [],
); );
@@ -60,10 +112,12 @@ class _MainAppState extends State<MainApp> {
children: [ children: [
Map(rayData), Map(rayData),
Player( Player(
onAngleUpdated: (angle) { rayData: rayData,
onAngleUpdated: (radians) {
// degrees = radians × (180 / π)
setState(() { setState(() {
rayData = rayData.copyWith( rayData = rayData.copyWith(
playerAngle: angle, playerAngle: radians,
); );
}); });
}, },
@@ -110,44 +164,133 @@ class Player extends StatefulWidget {
required this.onRaysUpdated, required this.onRaysUpdated,
required this.onAngleUpdated, required this.onAngleUpdated,
required this.onPositionUpdated, required this.onPositionUpdated,
required this.rayData,
super.key, super.key,
}); });
final ValueChanged<List<Ray>> onRaysUpdated; final ValueChanged<List<Ray>> onRaysUpdated;
final ValueChanged<double> onAngleUpdated; final ValueChanged<double> onAngleUpdated;
final ValueChanged<Offset> onPositionUpdated; final ValueChanged<Offset> onPositionUpdated;
final RayData rayData;
/// How "fast" the player moves
static const int speed = 5;
@override @override
State<Player> createState() => _PlayerState(); State<Player> createState() => _PlayerState();
} }
class _PlayerState extends State<Player> { class _PlayerState extends State<Player> {
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 @override
void initState() { void initState() {
super.initState(); super.initState();
_focusNode = FocusNode()..requestFocus();
HardwareKeyboard.instance.addHandler(keyPressHandler);
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onRaysUpdated(calculateRays(playerPosition, playerDelta)); widget.onRaysUpdated(
widget.onAngleUpdated(playerAngle); calculateRays(
widget.onPositionUpdated(playerPosition); widget.rayData.playerPosition,
widget.rayData.playerAngle.playerDelta,
),
);
widget.onAngleUpdated(
widget.rayData.playerAngle,
);
widget.onPositionUpdated(
widget.rayData.playerPosition,
);
}); });
} }
@override @override
void dispose() { void dispose() {
_focusNode.dispose();
super.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<LogicalKeyboardKey> 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<Ray> calculateRays(Offset offset, Offset delta) { List<Ray> calculateRays(Offset offset, Offset delta) {
@@ -368,56 +511,10 @@ class _PlayerState extends State<Player> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// TODO: Use HardwareKeyboard return CustomPaint(
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( painter: PlayerPainter(
offset: playerPosition, offset: widget.rayData.playerPosition,
delta: playerDelta, delta: widget.rayData.playerAngle.playerDelta,
),
), ),
); );
} }