Compare commits
10 Commits
a6911411f9
...
c36f151585
| Author | SHA1 | Date | |
|---|---|---|---|
|
c36f151585
|
|||
|
a35dbeddf0
|
|||
|
394b9acf3f
|
|||
|
63c2a1bfa2
|
|||
|
277903ace8
|
|||
|
c83e343dcf
|
|||
|
bdb8b1c67f
|
|||
|
ce0b2432c6
|
|||
|
024d60d25a
|
|||
|
3fee462c35
|
+566
-26
@@ -1,6 +1,7 @@
|
||||
import "dart:math" as math;
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
|
||||
// dart format off
|
||||
const List<int> map = [
|
||||
@@ -19,36 +20,83 @@ 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> {
|
||||
RayData rayData = const RayData(
|
||||
playerAngle: 0,
|
||||
playerPosition: Offset.zero,
|
||||
rays: [],
|
||||
);
|
||||
|
||||
@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: [
|
||||
Flexible(
|
||||
child: Center(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.contain,
|
||||
child: Stack(
|
||||
children: [
|
||||
Map(),
|
||||
Player(),
|
||||
],
|
||||
child: Transform.translate(
|
||||
offset: const Offset(16, 0),
|
||||
child: Center(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.contain,
|
||||
child: Stack(
|
||||
children: [
|
||||
Map(rayData),
|
||||
Player(
|
||||
onAngleUpdated: (angle) {
|
||||
setState(() {
|
||||
rayData = rayData.copyWith(
|
||||
playerAngle: angle,
|
||||
);
|
||||
});
|
||||
},
|
||||
onPositionUpdated: (position) {
|
||||
setState(() {
|
||||
rayData = rayData.copyWith(
|
||||
playerPosition: position,
|
||||
);
|
||||
});
|
||||
},
|
||||
onRaysUpdated: (rays) {
|
||||
setState(() {
|
||||
rayData = rayData.copyWith(
|
||||
rays: rays,
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Flexible(child: Background()),
|
||||
Flexible(
|
||||
flex: 3,
|
||||
child: Stack(
|
||||
children: [
|
||||
const Background(),
|
||||
Walls(rayData),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -58,42 +106,444 @@ 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<Ray>> onRaysUpdated;
|
||||
final ValueChanged<double> onAngleUpdated;
|
||||
final ValueChanged<Offset> onPositionUpdated;
|
||||
|
||||
@override
|
||||
State<Player> createState() => _PlayerState();
|
||||
}
|
||||
|
||||
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
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focusNode = FocusNode()..requestFocus();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.onRaysUpdated(calculateRays(playerPosition, playerDelta));
|
||||
widget.onAngleUpdated(playerAngle);
|
||||
widget.onPositionUpdated(playerPosition);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
List<Ray> calculateRays(Offset offset, Offset delta) {
|
||||
const double dr = math.pi / 180;
|
||||
|
||||
final List<Ray> rays = [];
|
||||
|
||||
int dof;
|
||||
int mx;
|
||||
int my;
|
||||
int mapPoint;
|
||||
|
||||
double yo = 1;
|
||||
double xo = 1;
|
||||
|
||||
final Offset playerCenter = offset + const Offset(4, 4);
|
||||
|
||||
double rayX = 0;
|
||||
double rayY = 0;
|
||||
double disT = 0;
|
||||
|
||||
double rayAngle = delta.direction - (dr * 30);
|
||||
|
||||
if (rayAngle < 0) {
|
||||
rayAngle += 2 * math.pi;
|
||||
}
|
||||
if (rayAngle > 2 * math.pi) {
|
||||
rayAngle -= 2 * math.pi;
|
||||
}
|
||||
|
||||
int r;
|
||||
for (r = 0; r < 60; r++) {
|
||||
// * --- Check Horizontal Lines ---
|
||||
dof = 0;
|
||||
double distanceHor = 1000000;
|
||||
double hx = playerCenter.dx;
|
||||
double hy = playerCenter.dy;
|
||||
|
||||
// Negative inverse of tangent
|
||||
final double aTan = -1.0 / math.tan(rayAngle);
|
||||
|
||||
// ? Looking up (ray angle > π)
|
||||
if (rayAngle > math.pi) {
|
||||
rayY = ((playerCenter.dy / mapSize).floorToDouble() * mapSize) - 0.0001;
|
||||
|
||||
rayX = (playerCenter.dy - rayY) * aTan + playerCenter.dx;
|
||||
|
||||
yo = -mapSize / 1;
|
||||
xo = -yo * aTan;
|
||||
}
|
||||
|
||||
// ? Looking down
|
||||
if (rayAngle < math.pi) {
|
||||
rayY =
|
||||
((playerCenter.dy / mapSize).floorToDouble() * mapSize) + mapSize;
|
||||
|
||||
rayX = (playerCenter.dy - rayY) * aTan + playerCenter.dx;
|
||||
|
||||
yo = mapSize / 1;
|
||||
xo = -yo * aTan;
|
||||
}
|
||||
|
||||
// ? Looking straight left or right
|
||||
if (rayAngle == 0.0 || rayAngle == math.pi) {
|
||||
// It's impossible to ever hit a horizontal line
|
||||
rayX = playerCenter.dx;
|
||||
rayY = playerCenter.dy;
|
||||
dof = 8;
|
||||
}
|
||||
|
||||
int mv = 0;
|
||||
int mh = 0;
|
||||
|
||||
while (dof < 8) {
|
||||
mx = rayX ~/ mapSize;
|
||||
my = rayY ~/ mapSize;
|
||||
|
||||
mapPoint = my * mapX + mx;
|
||||
|
||||
if (mapPoint > 0 && mapPoint < mapX * mapY && map[mapPoint] > 0) {
|
||||
// We hit a wall!
|
||||
|
||||
mh = map[mapPoint];
|
||||
|
||||
hx = rayX;
|
||||
hy = rayY;
|
||||
distanceHor = dist(
|
||||
playerCenter.dx,
|
||||
playerCenter.dy,
|
||||
hx,
|
||||
hy,
|
||||
rayAngle,
|
||||
);
|
||||
|
||||
dof = 8;
|
||||
} else {
|
||||
rayX += xo;
|
||||
rayY += yo;
|
||||
dof += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// * --- Check Vertical Lines ---
|
||||
dof = 0;
|
||||
double distanceVert = 1000000;
|
||||
double vx = playerCenter.dx;
|
||||
double vy = playerCenter.dy;
|
||||
|
||||
// Negative of tangent
|
||||
final double nTan = -math.tan(rayAngle);
|
||||
|
||||
const double pi2 = math.pi / 2;
|
||||
const double pi3 = 3 * math.pi / 2;
|
||||
|
||||
// ? Looking left
|
||||
if (rayAngle > pi2 && rayAngle < pi3) {
|
||||
rayX = ((playerCenter.dx / mapSize).floorToDouble() * mapSize) - 0.0001;
|
||||
rayY = (playerCenter.dx - rayX) * nTan + playerCenter.dy;
|
||||
xo = -mapSize / 1;
|
||||
yo = -xo * nTan;
|
||||
}
|
||||
|
||||
// ? Looking right
|
||||
if (rayAngle < pi2 || rayAngle > pi3) {
|
||||
rayX =
|
||||
((playerCenter.dx / mapSize).floorToDouble() * mapSize) + mapSize;
|
||||
rayY = (playerCenter.dx - rayX) * nTan + playerCenter.dy;
|
||||
xo = mapSize / 1;
|
||||
yo = -xo * nTan;
|
||||
}
|
||||
|
||||
// ? Looking straight up or down
|
||||
if (rayAngle == pi2 || rayAngle == pi3) {
|
||||
rayX = playerCenter.dx;
|
||||
rayY = playerCenter.dy;
|
||||
dof = 8;
|
||||
}
|
||||
|
||||
while (dof < 8) {
|
||||
mx = rayX ~/ mapSize;
|
||||
my = rayY ~/ mapSize;
|
||||
|
||||
mapPoint = my * mapX + mx;
|
||||
|
||||
if (mapPoint > 0 && mapPoint < mapX * mapY && map[mapPoint] > 0) {
|
||||
// We hit a wall!
|
||||
|
||||
mv = map[mapPoint];
|
||||
|
||||
// Save the values and calculate the distance from the player
|
||||
vx = rayX;
|
||||
vy = rayY;
|
||||
distanceVert = dist(
|
||||
playerCenter.dx,
|
||||
playerCenter.dy,
|
||||
vx,
|
||||
vy,
|
||||
rayAngle,
|
||||
);
|
||||
|
||||
dof = 8; // Break the loop - no need to calculate more.
|
||||
} else {
|
||||
// If we didn't hit a wall, we check the next horizontal line by adding
|
||||
// the x and y offset.
|
||||
rayX += xo;
|
||||
rayY += yo;
|
||||
dof += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Color wallColor = Colors.transparent;
|
||||
|
||||
if (distanceVert < distanceHor) {
|
||||
rayX = vx;
|
||||
rayY = vy;
|
||||
disT = distanceVert;
|
||||
// Vertical walls are drawn a little brighter than horizontal walls
|
||||
wallColor = const Color.fromRGBO(230, 0, 0, 1);
|
||||
|
||||
if (mv == 2) {
|
||||
wallColor = const Color.fromRGBO(0, 179, 0, 1);
|
||||
}
|
||||
|
||||
rays.add(Ray(Offset(rayX, rayY), disT, wallColor));
|
||||
}
|
||||
|
||||
if (distanceHor < distanceVert) {
|
||||
rayX = hx;
|
||||
rayY = hy;
|
||||
disT = distanceHor;
|
||||
// Horizontal walls are drawn a little darker than vertical walls
|
||||
wallColor = const Color.fromRGBO(179, 0, 0, 1);
|
||||
|
||||
if (mh == 2) {
|
||||
wallColor = const Color.fromRGBO(0, 102, 0, 1);
|
||||
}
|
||||
|
||||
rays.add(Ray(Offset(rayX, rayY), disT, wallColor));
|
||||
}
|
||||
|
||||
// * Increase the ray angle to draw the next ray
|
||||
rayAngle += dr;
|
||||
|
||||
if (rayAngle < 0) {
|
||||
rayAngle += 2 * math.pi;
|
||||
}
|
||||
if (rayAngle > 2 * math.pi) {
|
||||
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) {
|
||||
return CustomPaint(
|
||||
painter: PlayerPainter(offset: playerPosition),
|
||||
// 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) {
|
||||
final paintYelow = Paint()..color = Colors.yellow;
|
||||
|
||||
// Player dot
|
||||
canvas.drawRect(
|
||||
Rect.fromLTWH(offset.dx, offset.dy, 8, 8),
|
||||
paintYelow,
|
||||
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, {super.key});
|
||||
|
||||
final RayData rayData;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) => ClipRect(
|
||||
child: CustomPaint(
|
||||
size: constraints.biggest,
|
||||
painter: WallPainter(
|
||||
playerAngle: rayData.playerAngle,
|
||||
rayData: rayData.rays,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class WallPainter extends CustomPainter {
|
||||
final double playerAngle;
|
||||
final List<Ray> rayData;
|
||||
|
||||
const new({
|
||||
required this.playerAngle,
|
||||
|
||||
required this.rayData,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final double scale = math.max(
|
||||
size.width / gameWidth,
|
||||
size.height / gameHeight,
|
||||
);
|
||||
final double offsetX = (size.width - gameWidth * scale) / 2;
|
||||
final double offsetY = (size.height - gameHeight * scale) / 2;
|
||||
|
||||
canvas
|
||||
..save()
|
||||
..translate(offsetX, offsetY)
|
||||
..scale(scale);
|
||||
|
||||
for (int i = 0; i < rayData.length; i++) {
|
||||
final Ray ray = rayData[i];
|
||||
|
||||
// * --- Draw 3D Walls ---
|
||||
// Correct the fish-eye effect
|
||||
const double dr = math.pi / 180;
|
||||
final double rayAngle = playerAngle - (dr * 30) + (i * dr);
|
||||
|
||||
double correctedAngle = playerAngle - rayAngle;
|
||||
if (correctedAngle < 0) {
|
||||
correctedAngle += 2 * math.pi;
|
||||
}
|
||||
if (correctedAngle > 2 * math.pi) {
|
||||
correctedAngle -= 2 * math.pi;
|
||||
}
|
||||
|
||||
final double disT = ray.distance * 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 = i * rayWidth;
|
||||
|
||||
canvas.drawRect(
|
||||
Rect.fromLTWH(rayX3D, lineOffset, rayWidth, lineHeight),
|
||||
Paint()
|
||||
..color = ray.paintColor
|
||||
..isAntiAlias = false
|
||||
..strokeWidth = 1.5,
|
||||
);
|
||||
}
|
||||
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -104,21 +554,36 @@ class PlayerPainter extends CustomPainter {
|
||||
|
||||
// * Map
|
||||
class Map extends StatelessWidget {
|
||||
const new({super.key});
|
||||
const new(
|
||||
this.rayData, {
|
||||
super.key,
|
||||
});
|
||||
|
||||
final RayData rayData;
|
||||
|
||||
@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(
|
||||
rayData: rayData,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -140,6 +605,12 @@ class MapPainter extends CustomPainter {
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paintWhite = Paint()..color = Colors.white;
|
||||
final paintBlack = Paint()..color = Colors.black;
|
||||
final paintGrey = Paint()..color = Colors.grey;
|
||||
|
||||
canvas.drawRect(
|
||||
Rect.fromLTWH(0, 0, tileSize * mapX, tileSize * mapY),
|
||||
paintGrey,
|
||||
);
|
||||
|
||||
for (int i = 0; i < mapData.length; i++) {
|
||||
final double x = (i % mapX) * tileSize;
|
||||
@@ -158,6 +629,34 @@ class MapPainter extends CustomPainter {
|
||||
bool shouldRebuildSemantics(covariant oldDelegate) => false;
|
||||
}
|
||||
|
||||
class MapRayPainter extends CustomPainter {
|
||||
final RayData rayData;
|
||||
|
||||
const new({
|
||||
required this.rayData,
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final Offset playerCenter = rayData.playerPosition + const Offset(4, 4);
|
||||
|
||||
for (final Ray ray in rayData.rays) {
|
||||
canvas.drawLine(
|
||||
playerCenter,
|
||||
ray.hitPosition - const Offset(1, 1),
|
||||
Paint()
|
||||
..color = Colors.cyan
|
||||
..strokeWidth = 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(covariant oldDelegate) => false;
|
||||
@override
|
||||
bool shouldRebuildSemantics(covariant oldDelegate) => false;
|
||||
}
|
||||
|
||||
// * Background
|
||||
class Background extends StatelessWidget {
|
||||
const new({
|
||||
@@ -249,3 +748,44 @@ class Floor extends CustomPainter {
|
||||
@override
|
||||
bool shouldRebuildSemantics(covariant oldDelegate) => false;
|
||||
}
|
||||
|
||||
// * Ray Data
|
||||
class Ray(
|
||||
/// Where it hit on the 2D map
|
||||
var Offset hitPosition,
|
||||
|
||||
/// Corrected 3D distance
|
||||
var double distance,
|
||||
|
||||
/// Wall color
|
||||
var Color paintColor,
|
||||
);
|
||||
|
||||
class RayData {
|
||||
/// The direction the player is facing
|
||||
final double playerAngle;
|
||||
|
||||
/// The player's position
|
||||
final Offset playerPosition;
|
||||
|
||||
/// The rays cast from the player
|
||||
final List<Ray> rays;
|
||||
|
||||
const RayData({
|
||||
required this.playerAngle,
|
||||
required this.playerPosition,
|
||||
required this.rays,
|
||||
});
|
||||
|
||||
RayData copyWith({
|
||||
double? playerAngle,
|
||||
Offset? playerPosition,
|
||||
List<Ray>? rays,
|
||||
}) {
|
||||
return RayData(
|
||||
playerAngle: playerAngle ?? this.playerAngle,
|
||||
playerPosition: playerPosition ?? this.playerPosition,
|
||||
rays: rays ?? this.rays,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user