Abstract out math logic and reuse in multiple painters

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-08-30 21:10:25 +02:00
parent c83e343dcf
commit 277903ace8
+323 -137
View File
@@ -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<MainApp> createState() => _MainAppState();
}
class _MainAppState extends State<MainApp> {
List<RayData> 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<List<RayData>> onRaysUpdated;
final ValueChanged<double> onAngleUpdated;
final ValueChanged<Offset> onPositionUpdated;
@override
State<Player> createState() => _PlayerState();
@@ -80,6 +131,12 @@ class _PlayerState extends State<Player> {
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<Player> {
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<RayData> calculateRays(Offset offset, Offset delta) {
const double dr = math.pi / 180;
final List<RayData> 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> 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> 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> 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> 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,
);