Maps are blazingly fast now.

This commit is contained in:
Kapendev 2025-03-14 03:59:37 +02:00
parent 7846b4adb8
commit 557622f64f
4 changed files with 107 additions and 192 deletions

View file

@ -9,39 +9,31 @@ auto tile = Tile(16, 16, 145);
auto tileFlip = Flip.none;
void ready() {
lockResolution(320, 180);
lockResolution(160, 90);
atlas = loadTexture("parin_atlas.png");
// Parse a CSV string representing a tile map, where each tile is 16x16 pixels in size.
// Parse a CSV representing a tile map, where each tile is 16x16 pixels in size.
map.parse("-1,-1,-1\n21,22,23\n37,38,39\n53,54,55", 16, 16);
}
bool update(float dt) {
// Create the drawing options for the map and tile.
auto mapOptions = DrawOptions(Vec2(2));
auto tileOptions = mapOptions;
tileOptions.flip = tileFlip;
if (wasd.x > 0) tileFlip = Flip.x;
else if (wasd.x < 0) tileFlip = Flip.none;
// Move the tile and the camera.
// Move and update the game objects.
tileFlip = wasd.x ? (wasd.x > 0 ? Flip.x : Flip.none) : tileFlip;
tile.position += wasd * Vec2(120 * dt);
camera.position = tile.position + tile.size * Vec2(0.5f);
// Check for collisions between the tile and the map and resolve the collision.
foreach (position; map.gridPositions(camera.area, mapOptions)) {
if (map[position] < 0) continue;
auto area = Rect(map.worldPosition(position, mapOptions), map.tileSize * mapOptions.scale);
while (area.hasIntersection(Rect(tile.position, tile.size * mapOptions.scale))) {
// Check for collisions with the map and resolve them.
foreach (point; map.gridPoints(camera.area)) {
if (map[point] < 0) continue;
auto area = Rect(map.toWorldPoint(point), map.tileSize);
while (area.hasIntersection(tile.area)) {
tile.position -= wasd * Vec2(dt);
camera.position = tile.position + tile.size * Vec2(0.5f);
}
}
// Draw the tile and the map.
// Draw the world.
camera.attach();
drawTile(atlas, tile, tileOptions);
drawTileMap(atlas, map, camera, mapOptions);
drawTile(atlas, tile, DrawOptions(tileFlip));
drawTileMap(atlas, map, camera);
camera.detach();
drawDebugText("Move with arrow keys.", Vec2(8));
return false;
}

View file

@ -311,11 +311,8 @@ struct TextureId {
/// Retrieves the texture associated with the resource identifier.
ref Texture get() {
if (!isValid) {
if (data.value) {
assert(0, "ID `{}` with generation `{}` does not exist.".format(data.value, data.generation));
} else {
assert(0, "ID `0` is always invalid and represents a resource that was never created.");
}
if (data.value) assert(0, "ID `{}` with generation `{}` does not exist.".format(data.value, data.generation));
else assert(0, "ID `0` is always invalid and represents a resource that was never created.");
}
return engineState.textures[GenerationalIndex(data.value - 1, data.generation)];
}
@ -414,11 +411,8 @@ struct FontId {
/// Retrieves the font associated with the resource identifier.
ref Font get() {
if (!isValid) {
if (data.value) {
assert(0, "ID `{}` with generation `{}` does not exist.".format(data.value, data.generation));
} else {
assert(0, "ID `0` is always invalid and represents a resource that was never created.");
}
if (data.value) assert(0, "ID `{}` with generation `{}` does not exist.".format(data.value, data.generation));
else assert(0, "ID `0` is always invalid and represents a resource that was never created.");
}
return engineState.fonts[GenerationalIndex(data.value - 1, data.generation)];
}
@ -585,11 +579,8 @@ struct SoundId {
/// Retrieves the sound associated with the resource identifier.
ref Sound get() {
if (!isValid) {
if (data.value) {
assert(0, "ID `{}` with generation `{}` does not exist.".format(data.value, data.generation));
} else {
assert(0, "ID `0` is always invalid and represents a resource that was never created.");
}
if (data.value) assert(0, "ID `{}` with generation `{}` does not exist.".format(data.value, data.generation));
else assert(0, "ID `0` is always invalid and represents a resource that was never created.");
}
return engineState.sounds[GenerationalIndex(data.value - 1, data.generation)];
}
@ -2099,6 +2090,7 @@ void drawLine(Line area, float size, Color color = white) {
void drawTextureArea(Texture texture, Rect area, Vec2 position, DrawOptions options = DrawOptions()) {
if (texture.isEmpty || area.size.x <= 0.0f || area.size.y <= 0.0f) return;
auto target = Rect(position, area.size * options.scale.abs());
auto origin = options.origin == Vec2() ? target.origin(options.hook) : options.origin;
auto flip = options.flip;
if (options.scale.x < 0.0f && options.scale.y < 0.0f) {
flip = oppositeFlip(flip, Flip.xy);
@ -2113,8 +2105,6 @@ void drawTextureArea(Texture texture, Rect area, Vec2 position, DrawOptions opti
case Flip.y: area.size.y *= -1.0f; break;
case Flip.xy: area.size *= Vec2(-1.0f); break;
}
auto origin = options.origin == Vec2() ? target.origin(options.hook) : options.origin;
if (isPixelSnapped || isPixelPerfect) {
rl.DrawTexturePro(
texture.data,
@ -2419,12 +2409,9 @@ mixin template runGame(alias readyFunc, alias updateFunc, alias finishFunc, int
extern(C)
void main(int argc, immutable(char)** argv) {
openWindow(width, height, [], title);
// Yeah... I love writing code again and again and again.
foreach (i; 0 .. argc) {
Sz length = 0;
while (argv[i][length] != '\0') {
length += 1;
}
while (argv[i][length] != '\0') length += 1;
engineState.envArgsBuffer.append(argv[i][0 .. length]);
}
readyFunc();

View file

@ -36,6 +36,10 @@ struct Tile {
return Vec2(width, height);
}
Rect area() {
return Rect(position, size);
}
Sz row(Sz colCount) {
return id / colCount;
}
@ -179,32 +183,6 @@ struct TileMap {
return Vec2(tileWidth, tileHeight);
}
Fault parse(IStr csv, int newTileWidth, int newTileHeight) {
if (csv.length == 0) return Fault.invalid;
if (data.isEmpty) {
data.resizeBlank(defaultGridRowCount, defaultGridColCount);
}
tileWidth = newTileWidth;
tileHeight = newTileHeight;
softRowCount = 0;
softColCount = 0;
auto view = csv;
while (view.length != 0) {
softRowCount += 1;
softColCount = 0;
if (softRowCount > data.rowCount) return Fault.invalid;
auto line = view.skipLine();
while (line.length != 0) {
softColCount += 1;
if (softColCount > data.colCount) return Fault.invalid;
auto tile = line.skipValue(',').toSigned();
if (tile.isNone) return Fault.invalid;
data[softRowCount - 1, softColCount - 1] = cast(short) tile.get();
}
}
return Fault.none;
}
/// Moves the tile map to follow the target position at the specified speed.
void followPosition(Vec2 target, float speed) {
position = position.moveTo(target, Vec2(speed));
@ -216,7 +194,7 @@ struct TileMap {
}
/// Returns the top left world position of a grid position.
Vec2 worldPosition(Sz row, Sz col, DrawOptions options = DrawOptions()) {
Vec2 toWorldPoint(Sz row, Sz col, DrawOptions options = DrawOptions()) {
auto targetTileWidth = cast(int) (tileWidth * options.scale.x);
auto targetTileHeight = cast(int) (tileHeight * options.scale.y);
auto temp = Rect(
@ -229,32 +207,11 @@ struct TileMap {
}
/// Returns the top left world position of a grid position.
Vec2 worldPosition(IVec2 gridPosition, DrawOptions options = DrawOptions()) {
return worldPosition(gridPosition.y, gridPosition.x, options);
Vec2 toWorldPoint(IVec2 gridPosition, DrawOptions options = DrawOptions()) {
return toWorldPoint(gridPosition.y, gridPosition.x, options);
}
IVec2 firstGridPosition(Vec2 topLeftWorldPosition, DrawOptions options = DrawOptions()) {
if (softRowCount == 0 || softColCount == 0) return IVec2();
auto result = IVec2();
auto targetTileWidth = cast(int) (tileWidth * options.scale.x);
auto targetTileHeight = cast(int) (tileHeight * options.scale.y);
result.y = cast(int) floor(clamp((topLeftWorldPosition.y - position.y) / targetTileHeight, 0, softRowCount - 1));
result.x = cast(int) floor(clamp((topLeftWorldPosition.x - position.x) / targetTileWidth, 0, softColCount - 1));
return result;
}
IVec2 lastGridPosition(Vec2 bottomRightWorldPosition, DrawOptions options = DrawOptions()) {
if (softRowCount == 0 || softColCount == 0) return IVec2();
auto result = IVec2();
auto targetTileWidth = cast(int) (tileWidth * options.scale.x);
auto targetTileHeight = cast(int) (tileHeight * options.scale.y);
auto extraTileCount = options.hook == Hook.topLeft ? 1 : 2;
result.y = cast(int) floor(clamp((bottomRightWorldPosition.y - position.y) / targetTileHeight + extraTileCount, 0, softRowCount - 1));
result.x = cast(int) floor(clamp((bottomRightWorldPosition.x - position.x) / targetTileWidth + extraTileCount, 0, softColCount - 1));
return result;
}
auto gridPositions(Vec2 topLeftWorldPosition, Vec2 bottomRightWorldPosition, DrawOptions options = DrawOptions()) {
auto gridPoints(Vec2 topLeftWorldPoint, Vec2 bottomRightWorldPoint, DrawOptions options = DrawOptions()) {
static struct Range {
Sz colCount;
IVec2 first;
@ -278,17 +235,52 @@ struct TileMap {
}
}
auto result = Range(
softColCount,
firstGridPosition(topLeftWorldPosition, options),
lastGridPosition(bottomRightWorldPosition, options),
if (softRowCount == 0 || softColCount == 0) return Range();
auto targetTileWidth = cast(int) (tileWidth * options.scale.x);
auto targetTileHeight = cast(int) (tileHeight * options.scale.y);
auto extraTileCount = options.hook == Hook.topLeft ? 1 : 2;
auto firstGridPoint = IVec2(
cast(int) clamp((topLeftWorldPoint.x - position.x) / targetTileWidth, 0, softColCount - 1),
cast(int) clamp((topLeftWorldPoint.y - position.y) / targetTileHeight, 0, softRowCount - 1),
);
auto lastGridPoint = IVec2(
cast(int) clamp((bottomRightWorldPoint.x - position.x) / targetTileWidth + extraTileCount, 0, softColCount - 1),
cast(int) clamp((bottomRightWorldPoint.y - position.y) / targetTileHeight + extraTileCount, 0, softRowCount - 1),
);
return Range(
softColCount,
firstGridPoint,
lastGridPoint,
firstGridPoint,
);
result.position = result.first;
return result;
}
auto gridPositions(Rect worldArea, DrawOptions options = DrawOptions()) {
return gridPositions(worldArea.topLeftPoint, worldArea.bottomRightPoint, options);
auto gridPoints(Rect worldArea, DrawOptions options = DrawOptions()) {
return gridPoints(worldArea.topLeftPoint, worldArea.bottomRightPoint, options);
}
Fault parse(IStr csv, int newTileWidth, int newTileHeight) {
if (csv.length == 0) return Fault.invalid;
if (data.isEmpty) data.resizeBlank(defaultGridRowCount, defaultGridColCount);
tileWidth = newTileWidth;
tileHeight = newTileHeight;
softRowCount = 0;
softColCount = 0;
auto view = csv;
while (view.length != 0) {
softRowCount += 1;
softColCount = 0;
if (softRowCount > data.rowCount) return Fault.invalid;
auto line = view.skipLine();
while (line.length != 0) {
softColCount += 1;
if (softColCount > data.colCount) return Fault.invalid;
auto tile = line.skipValue(',').toSigned();
if (tile.isNone) return Fault.invalid;
data[softRowCount - 1, softColCount - 1] = cast(short) tile.get();
}
}
return Fault.none;
}
}
@ -314,15 +306,21 @@ void drawTile(TextureId texture, Tile tile, DrawOptions options = DrawOptions())
}
void drawTileMap(Texture texture, TileMap map, Camera camera, DrawOptions options = DrawOptions()) {
if (texture.isEmpty || map.tileWidth <= 0 || map.tileHeight <= 0) return;
if (texture.isEmpty || map.softRowCount == 0 || map.softColCount == 0 || map.tileWidth <= 0 || map.tileHeight <= 0) return;
auto topLeftWorldPoint = camera.topLeftPoint;
auto bottomRightWorldPoint = camera.bottomRightPoint;
auto textureColCount = texture.width / map.tileWidth;
auto targetTileWidth = cast(int) (map.tileWidth * options.scale.x);
auto targetTileHeight = cast(int) (map.tileHeight * options.scale.y);
auto colRow1 = map.firstGridPosition(camera.topLeftPoint, options);
auto colRow2 = map.lastGridPosition(camera.bottomRightPoint, options);
if (colRow1.x == colRow2.x || colRow1.y == colRow2.y) return;
auto extraTileCount = options.hook == Hook.topLeft ? 1 : 2;
auto colRow1 = IVec2(
cast(int) clamp((topLeftWorldPoint.x - map.position.x) / targetTileWidth, 0, map.softColCount - 1),
cast(int) clamp((topLeftWorldPoint.y - map.position.y) / targetTileHeight, 0, map.softRowCount - 1),
);
auto colRow2 = IVec2(
cast(int) clamp((bottomRightWorldPoint.x - map.position.x) / targetTileWidth + extraTileCount, 0, map.softColCount - 1),
cast(int) clamp((bottomRightWorldPoint.y - map.position.y) / targetTileHeight + extraTileCount, 0, map.softRowCount - 1),
);
auto textureArea = Rect(map.tileWidth, map.tileHeight);
foreach (row; colRow1.y .. colRow2.y + 1) {
foreach (col; colRow1.x .. colRow2.x + 1) {

View file

@ -8,7 +8,7 @@
// TODO: Update all the doc comments here.
// TODO: Add one-way collision support for moving walls.
// TODO: Add spatial partitioning.
// TODO: Add spatial partitioning. Just check the cell a box is in and the cells that are near it.
// NOTE: Was working on spatial partitioning. The grid is done, just need to add values in it.
/// The `platformer` module provides a pixel-perfect physics engine.
@ -146,127 +146,65 @@ struct BoxWorld {
@safe @nogc nothrow:
@trusted
void appendWallIdToSpatialGrid(WallBoxId id) {
FixedList!(IVec2, 4) vecSet = void;
// vecSet.clear();
// auto taggedId = id & ~(1 << 31);
// foreach (position; getWallSpatialGridPositions) {
// auto canAppend = true;
// foreach (vec; vecSet) {
// if (vec == position) {
// canAppend = false;
// break;
// }
// }
// if (canAppend) {
// grid[vec.y, vec.x].append(taggedId);
// vecSet.append(vec);
// }
// }
}
void removeWallIdFromSpatialGrid(WallBoxId id) {
}
void enableSpatialGrid(Sz rowCount, Sz colCount, int tileWidth, int tileHeight) {
void enableGrid(Sz rowCount, Sz colCount, int tileWidth, int tileHeight) {
gridTileWidth = tileWidth;
gridTileHeight = tileHeight;
grid.resizeBlank(rowCount, colCount);
foreach (ref group; grid) {
group.length = 0;
}
foreach (ref group; grid) group.clear();
foreach (i, ref properties; wallsProperties) {
auto id = cast(BaseBoxId) (i + 1);
auto tagged = id & ~(1 << 31);
auto positions = getWallSpatialGridPositions(id);
// grid[positions[0].y, positions[0].x].append(tagged);
// if (positions[0] != positions[1]) {
// grid[positions[1].y, positions[1].x].append(tagged);
// }
auto point = getWallGridPoint(id);
grid[point.y, point.x].append(id & ~(1 << 31));
}
foreach (i, ref properties; actorsProperties) {
auto id = cast(BaseBoxId) (i + 1);
auto tagged = id | (1 << 31);
auto positions = getActorSpatialGridPositions(id);
// grid[positions[0].y, positions[0].x].append(tagged);
// if (positions[0] != positions[1]) {
// grid[positions[1].y, positions[1].x].append(tagged);
// }
auto point = getActorGridPoint(id);
grid[point.y, point.x].append(id | (1 << 31));
}
}
void disableSpatialGrid() {
void disableGrid() {
gridTileWidth = 0;
gridTileHeight = 0;
grid.clear();
}
ref IRect getWall(WallBoxId id) {
if (id == 0) {
assert(0, "ID `0` is always invalid and represents a box that was never created.");
} else if (id > walls.length) {
assert(0, "ID `{}` does not exist.".format(id));
}
if (id == 0) assert(0, "ID `0` is always invalid and represents a box that was never created.");
return walls[id - 1];
}
ref WallBoxProperties getWallProperties(WallBoxId id) {
if (id == 0) {
assert(0, "ID `0` is always invalid and represents a box that was never created.");
} else if (id > wallsProperties.length) {
assert(0, "ID `{}` does not exist.".format(id));
}
if (id == 0) assert(0, "ID `0` is always invalid and represents a box that was never created.");
return wallsProperties[id - 1];
}
@trusted
IVec2[4] getWallSpatialGridPositions(WallBoxId id) {
IVec2[4] result = void;
IVec2 getWallGridPoint(WallBoxId id) {
if (id == 0) assert(0, "ID `0` is always invalid and represents a box that was never created.");
auto i = id - 1;
result[0].x = walls[i].position.x / gridTileWidth - (walls[i].position.x < 0);
result[0].y = walls[i].position.y / gridTileHeight - (walls[i].position.y < 0);
result[3].x = (walls[i].position.x + walls[i].size.x) - ((walls[i].position.x + walls[i].size.x) < 0);
result[3].y = (walls[i].position.y + walls[i].size.y) - ((walls[i].position.y + walls[i].size.y) < 0);
result[1].x = result[3].x;
result[1].y = result[0].y;
result[2].x = result[0].x;
result[2].y = result[3].y;
return result;
return IVec2(
walls[i].position.x / gridTileWidth - (walls[i].position.x < 0),
walls[i].position.y / gridTileHeight - (walls[i].position.y < 0),
);
}
ref IRect getActor(ActorBoxId id) {
if (id == 0) {
assert(0, "ID `0` is always invalid and represents a box that was never created.");
} else if (id > actors.length) {
assert(0, "ID `{}` does not exist.".format(id));
}
if (id == 0) assert(0, "ID `0` is always invalid and represents a box that was never created.");
return actors[id - 1];
}
ref ActorBoxProperties getActorProperties(ActorBoxId id) {
if (id == 0) {
assert(0, "ID `0` is always invalid and represents a box that was never created.");
} else if (id > actorsProperties.length) {
assert(0, "ID `{}` does not exist.".format(id));
}
if (id == 0) assert(0, "ID `0` is always invalid and represents a box that was never created.");
return actorsProperties[id - 1];
}
@trusted
IVec2[4] getActorSpatialGridPositions(WallBoxId id) {
IVec2[4] result = void;
IVec2 getActorGridPoint(ActorBoxId id) {
if (id == 0) assert(0, "ID `0` is always invalid and represents a box that was never created.");
auto i = id - 1;
result[0].x = actors[i].position.x / gridTileWidth - (actors[i].position.x < 0);
result[0].y = actors[i].position.y / gridTileHeight - (actors[i].position.y < 0);
result[1].x = (actors[i].position.x + actors[i].size.x) - ((actors[i].position.x + actors[i].size.x) < 0);
result[1].y = (actors[i].position.y + actors[i].size.y) - ((actors[i].position.y + actors[i].size.y) < 0);
result[1].x = result[3].x;
result[1].y = result[0].y;
result[2].x = result[0].x;
result[2].y = result[3].y;
return result;
return IVec2(
actors[i].position.x / gridTileWidth - (actors[i].position.x < 0),
actors[i].position.y / gridTileHeight - (actors[i].position.y < 0),
);
}
WallBoxId appendWall(IRect box, OneWaySide oneWaySide = OneWaySide.none) {