Fixed clipping through walls; map is now an overlay

Signed-off-by: Hans Kokx <hans.d.kokx@gmail.com>
This commit is contained in:
2026-09-13 17:11:56 +02:00
parent 2f6db48e4a
commit 6e566c339d
+316 -257
View File
@@ -3,23 +3,6 @@ import "dart:math" as math;
import "package:flutter/services.dart";
import "package:flutter/widgets.dart";
// dart format off
const List<int> map = [
1,1,1,1,1,1,1,1,
1,0,0,0,0,0,0,1,
1,0,1,0,0,0,0,1,
1,0,1,0,0,0,0,1,
1,0,0,0,0,0,0,1,
1,0,0,0,0,2,0,1,
1,0,0,0,0,0,0,1,
1,1,1,1,1,1,1,1,
];
// dart format on
const int mapX = 8;
const int mapY = 8;
const int mapSize = 32;
const double gameWidth = 320;
const double gameHeight = 256;
@@ -67,13 +50,15 @@ extension GameValues on double {
extension WorldValues on Offset {
// The current "tile" we're in on the map
Offset get currentTile => Offset(
(dx / mapSize).floorToDouble(),
(dy / mapSize).floorToDouble(),
(dx / GameMap.size).floorToDouble(),
(dy / GameMap.size).floorToDouble(),
);
// The index of the current "tile" we're in
// index = row * stride + column
int get index => (currentTile.dy.toInt() * mapX) + currentTile.dx.toInt();
int get index =>
(currentTile.dy.toInt() * GameMap.dimensions.width.toInt()) +
currentTile.dx.toInt();
}
void main() {
@@ -90,7 +75,7 @@ class MainApp extends StatefulWidget {
class _MainAppState extends State<MainApp> {
RayData rayData = const RayData(
playerAngle: 0,
playerPosition: Offset(1.5 * mapSize, 1.5 * mapSize),
playerPosition: Offset(1.5 * GameMap.size, 1.5 * GameMap.size),
rays: [],
);
@@ -99,16 +84,19 @@ class _MainAppState extends State<MainApp> {
return Directionality(
textDirection: TextDirection.ltr,
child: LayoutBuilder(
builder: (context, constraints) => Row(
spacing: mapSize / 1,
builder: (context, constraints) => Stack(
textDirection: TextDirection.ltr,
children: [
Flexible(
child: Transform.translate(
offset: const Offset(16, 0),
child: Center(
child: FittedBox(
fit: BoxFit.contain,
Stack(
children: [
const Background(),
Walls(rayData),
],
),
Opacity(
opacity: 0.5,
child: SizedBox.square(
dimension: GameMap.size * GameMap.dimensions.width / 2,
child: Stack(
children: [
Map(rayData),
@@ -141,17 +129,6 @@ class _MainAppState extends State<MainApp> {
),
),
),
),
),
Flexible(
flex: 3,
child: Stack(
children: [
const Background(),
Walls(rayData),
],
),
),
],
),
),
@@ -214,25 +191,38 @@ class _PlayerState extends State<Player> {
/// 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();
final int col = (position.dx / GameMap.size).floor();
final int row = (position.dy / GameMap.size).floor();
if (col < 0 || col >= mapX || row < 0 || row >= mapY) return 1; // solid
if (col < 0 ||
col >= GameMap.dimensions.width ||
row < 0 ||
row >= GameMap.dimensions.height)
return 1; // solid
return map[row * mapX + col];
return GameMap.map[row * GameMap.dimensions.width.toInt() + 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;
final List<double> choices = [
RayHit.castRay(
position,
direction.dx,
).distance,
RayHit.castRay(
position,
direction.direction.normalizeAngle - 0.05,
).distance,
RayHit.castRay(
position,
direction.direction.normalizeAngle + 0.05,
).distance,
];
for (double d = 0; d < maxCheck; d += stepSize) {
final Offset p = position + Offset(direction.dx, direction.dy) * d;
if (tileAt(p) > 0) return d;
}
choices.sort();
return double.infinity;
return choices.first;
}
bool keyPressHandler(KeyEvent event) {
@@ -243,6 +233,9 @@ class _PlayerState extends State<Player> {
// Where is the player now?
Offset playerPosition = widget.rayData.playerPosition;
final Offset playerCenter =
playerPosition +
Offset(GameMap.dimensions.width / 2, GameMap.dimensions.height / 2);
double playerAngle = widget.rayData.playerAngle;
// How many pixels away from a wall to stay
@@ -262,14 +255,13 @@ class _PlayerState extends State<Player> {
final Offset direction = playerAngle.direction;
if (keys.contains(LogicalKeyboardKey.keyW)) {
if (safeDistanceToWall(playerPosition, direction) >
Player.speed + buffer) {
if (safeDistanceToWall(playerCenter, direction) > Player.speed + buffer) {
// Move forward
playerPosition += playerAngle.playerDelta;
}
handled = true;
} else if (keys.contains(LogicalKeyboardKey.keyS)) {
if (safeDistanceToWall(playerPosition, -direction) >
if (safeDistanceToWall(playerCenter, -direction) >
Player.speed + buffer) {
// Move backwwards
playerPosition -= playerAngle.playerDelta;
@@ -291,198 +283,29 @@ class _PlayerState extends State<Player> {
List<Ray> calculateRays(Offset offset, double playerAngle) {
const double dr = math.pi / 180;
final double baseAngle = (playerAngle - dr * 30).normalizeAngle;
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 = (playerAngle - (dr * 30)).normalizeAngle;
final Offset playerCenter =
offset +
Offset(
GameMap.dimensions.width / 2,
GameMap.dimensions.height / 2,
);
int r;
for (r = 0; r < 60; r++) {
// * --- Check Horizontal Lines ---
dof = 0;
double distanceHor = 1000000;
double hx = playerCenter.dx;
double hy = playerCenter.dy;
final double angle = (baseAngle + r * dr).normalizeAngle;
final RayHit hit = RayHit.castRay(playerCenter, angle);
// 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,
rays.add(
Ray(
hit.hitPosition,
hit.distance,
hit.colorFor(hit.tileValue, hit.axis),
),
);
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 = const Color.fromRGBO(0, 0, 0, 0);
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 =
(playerAngle.normalizeAngle - (dr * 30) + r * dr).normalizeAngle;
}
return rays;
@@ -595,7 +418,7 @@ class WallPainter extends CustomPainter {
final double disT = ray.distance * math.cos(correctedAngle);
double lineHeight = (mapSize * gameHeight) / disT;
double lineHeight = (GameMap.size * gameHeight) / disT;
if (lineHeight > gameHeight) {
lineHeight = gameHeight;
}
@@ -621,9 +444,9 @@ class WallPainter extends CustomPainter {
}
@override
bool shouldRepaint(covariant oldDelegate) => false;
bool shouldRepaint(covariant oldDelegate) => true;
@override
bool shouldRebuildSemantics(covariant oldDelegate) => false;
bool shouldRebuildSemantics(covariant oldDelegate) => true;
}
// * Map
@@ -637,18 +460,15 @@ class Map extends StatelessWidget {
@override
Widget build(BuildContext context) {
const double width = mapX * mapSize / 1;
const double height = mapY * mapSize / 1;
return Stack(
children: [
const CustomPaint(
size: Size(width, height),
CustomPaint(
size: GameMap.dimensions,
painter: MapPainter(
mapData: map,
mapX: mapX,
mapY: mapY,
tileSize: mapSize / 1,
mapData: GameMap.map,
mapX: GameMap.dimensions.width,
mapY: GameMap.dimensions.height,
tileSize: GameMap.size / 1,
),
),
@@ -664,8 +484,8 @@ class Map extends StatelessWidget {
class MapPainter extends CustomPainter {
final List<int> mapData;
final int mapX;
final int mapY;
final double mapX;
final double mapY;
final double tileSize;
const new({
@@ -698,9 +518,9 @@ class MapPainter extends CustomPainter {
}
@override
bool shouldRepaint(covariant oldDelegate) => false;
bool shouldRepaint(covariant oldDelegate) => true;
@override
bool shouldRebuildSemantics(covariant oldDelegate) => false;
bool shouldRebuildSemantics(covariant oldDelegate) => true;
}
class MapRayPainter extends CustomPainter {
@@ -726,9 +546,9 @@ class MapRayPainter extends CustomPainter {
}
@override
bool shouldRepaint(covariant oldDelegate) => false;
bool shouldRepaint(covariant oldDelegate) => true;
@override
bool shouldRebuildSemantics(covariant oldDelegate) => false;
bool shouldRebuildSemantics(covariant oldDelegate) => true;
}
// * Background
@@ -823,7 +643,7 @@ class Floor extends CustomPainter {
bool shouldRebuildSemantics(covariant oldDelegate) => false;
}
// * Ray Data
// * Rays
class Ray(
/// Where it hit on the 2D map
var Offset hitPosition,
@@ -863,3 +683,242 @@ class RayData {
);
}
}
class RayHit(
/// Exact boundary crossing where the ray stopped
var Offset hitPosition,
/// Euclidean, from ray origin
var double distance,
/// 0 means "no hit within map"
var int tileValue,
/// [Axis] of the corresponding ray hit
var Axis axis,
) {
factory castRay(Offset origin, double angle) {
// 1. run horizontal-axis DDA → hit position + distance (or none)
// 2. run vertical-axis DDA → hit position + distance (or none)
// 3. return the nearer one
final RayHit hitHorizontal = RayHit._marchHorizontal(origin, angle);
final RayHit hitVertical = RayHit._marchVertical(origin, angle);
if (hitVertical.distance < hitHorizontal.distance) {
return hitVertical;
}
return hitHorizontal;
}
factory _marchHorizontal(Offset origin, double angle) {
// * --- Check Horizontal Lines ---
int dof = 0;
double distanceHor = double.infinity;
Offset hit = origin;
// Negative inverse of tangent
final double aTan = -1.0 / math.tan(angle);
double rayY = 0;
double rayX = 0;
double yOffset = 0;
double xOffset = 0;
// ? Looking up (ray angle > π)
if (angle > math.pi) {
rayY =
((origin.dy / GameMap.size).floorToDouble() * GameMap.size) - 0.0001;
rayX = (origin.dy - rayY) * aTan + origin.dx;
yOffset = -GameMap.size / 1;
xOffset = -yOffset * aTan;
}
// ? Looking down
if (angle < math.pi) {
rayY =
((origin.dy / GameMap.size).floorToDouble() * GameMap.size) +
GameMap.size;
rayX = (origin.dy - rayY) * aTan + origin.dx;
yOffset = GameMap.size / 1;
xOffset = -yOffset * aTan;
}
int mapPointX = 0;
int mapPointY = 0;
int mapPoint = 0;
int mapHit = 0;
while (dof < 8) {
mapPointX = rayX ~/ GameMap.size;
mapPointY = rayY ~/ GameMap.size;
mapPoint = mapPointY * GameMap.dimensions.width.toInt() + mapPointX;
if (mapPoint > 0 &&
mapPoint <
GameMap.dimensions.width.toInt() *
GameMap.dimensions.height.toInt() &&
GameMap.map[mapPoint] > 0) {
// We hit a wall!
mapHit = GameMap.map[mapPoint];
hit = Offset(rayX, rayY);
distanceHor = dist(
origin,
hit,
angle,
);
dof = 8;
} else {
rayX += xOffset;
rayY += yOffset;
dof += 1;
}
}
return RayHit(hit, distanceHor, mapHit, Axis.horizontal);
}
factory _marchVertical(Offset origin, double angle) {
// * --- Check Vertical Lines ---
int dof = 0;
double distanceVert = double.infinity;
Offset hit = origin;
// Negative of tangent
final double nTan = -math.tan(angle);
const double pi2 = math.pi / 2;
const double pi3 = 3 * math.pi / 2;
double rayX = 0;
double rayY = 0;
double xOffset = 0;
double yOffset = 0;
// ? Looking left
if (angle > pi2 && angle < pi3) {
rayX =
((origin.dx / GameMap.size).floorToDouble() * GameMap.size) - 0.0001;
rayY = (origin.dx - rayX) * nTan + origin.dy;
xOffset = -GameMap.size / 1;
yOffset = -xOffset * nTan;
}
// ? Looking right
if (angle < pi2 || angle > pi3) {
rayX =
((origin.dx / GameMap.size).floorToDouble() * GameMap.size) +
GameMap.size;
rayY = (origin.dx - rayX) * nTan + origin.dy;
xOffset = GameMap.size / 1;
yOffset = -xOffset * nTan;
}
// ? Looking straight up or down
if (angle == pi2 || angle == pi3) {
rayX = origin.dx;
rayY = origin.dy;
dof = 8;
}
int mapPointX = 0;
int mapPointY = 0;
int mapPoint = 0;
int mapHit = 0;
while (dof < 8) {
mapPointX = rayX ~/ GameMap.size;
mapPointY = rayY ~/ GameMap.size;
mapPoint = mapPointY * GameMap.dimensions.width.toInt() + mapPointX;
if (mapPoint > 0 &&
mapPoint <
GameMap.dimensions.width.toInt() *
GameMap.dimensions.height.toInt() &&
GameMap.map[mapPoint] > 0) {
// We hit a wall!
mapHit = GameMap.map[mapPoint];
// Save the values and calculate the distance from the player
hit = Offset(rayX, rayY);
distanceVert = dist(
origin,
hit,
angle,
);
// Break the loop - no need to calculate more.
dof = 8;
} else {
// If we didn't hit a wall, we check the next horizontal line by adding
// the x and y offset.
rayX += xOffset;
rayY += yOffset;
dof += 1;
}
}
return RayHit(hit, distanceVert, mapHit, Axis.vertical);
}
static double dist(Offset a, Offset b, double ang) {
return math.sqrt(
(b.dx - a.dx) * (b.dx - a.dx) + (b.dy - a.dy) * (b.dy - a.dy),
);
}
Color colorFor(int tileValue, Axis axis) {
Color wallColor = const Color.fromRGBO(0, 0, 0, 0);
if (axis == Axis.vertical) {
// Vertical walls are drawn a little brighter than horizontal walls
wallColor = const Color.fromRGBO(230, 0, 0, 1);
if (tileValue == 2) {
wallColor = const Color.fromRGBO(0, 179, 0, 1);
}
}
if (axis == Axis.horizontal) {
// Horizontal walls are drawn a little darker than vertical walls
wallColor = const Color.fromRGBO(179, 0, 0, 1);
if (tileValue == 2) {
wallColor = const Color.fromRGBO(0, 102, 0, 1);
}
}
return wallColor;
}
}
// * Map
abstract class GameMap {
// dart format off
static const List<int> map = [
1,1,1,1,1,1,1,1,
1,0,0,0,0,0,0,1,
1,0,1,0,0,0,0,1,
1,0,1,0,0,0,0,1,
1,0,0,0,0,0,0,1,
1,0,0,0,0,2,0,1,
1,0,0,0,0,0,0,1,
1,1,1,1,1,1,1,1,
];
// dart format on
static const Size dimensions = Size(8, 8);
static const int size = 32;
}