First pass at Flutter collision checks
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
+165
-68
@@ -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(
|
painter: PlayerPainter(
|
||||||
focusNode: _focusNode,
|
offset: widget.rayData.playerPosition,
|
||||||
onKeyEvent: (FocusNode node, value) {
|
delta: widget.rayData.playerAngle.playerDelta,
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user