Abstract out math logic and reuse in multiple painters
Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
+323
-137
@@ -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,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user