Generation and Render of 2D Map with Terrain in Dart + Flutter. Adding Lakes. Part 3

Search for a command to run...

No comments yet. Be the first to comment.
This is the series related to my experience building game with Flutter and Dart. It is basically a diary. At first I implement feature and then add a post about it. https://locadederta.com/sloboda
Introduction In previous post we finished all the logic related to generation of the map. To be able to apply this feature in real game we need to save and restore the generated map. If we do not do this then each time player starts the game a new ...
Build. Craft. Explore.

Заселяємо південь України!

Hey! We just published the second content update for our game Sloboda 2. This update is free for anyone who already purchased our game. If you want to purchase it, then pick your platforms: Apple App Store: https://apps.apple.com/us/app/loca-deserta-...

Привіт, компана! Ось і настав час Січневого оновлення для нашої гри Слобода 2! Це оновлення безкоштовне для всіх, хто придбав гру. Якщо ж ще не придбали, то можете це зробити по цих посиланнях: Apple App Store: https://apps.apple.com/ua/app/loca-dese...

Статистика запуску гри Трохи більше двох тижнів тому ми випустили гру на всіх магазинах. І одразу ж попали в Топ платних ігор 🇺🇦українського App Store! Купити можна на iOS по посиланню: https://apps.apple.com/ua/app/loca-deserta-sloboda-2/id164250...

On this page
In Previous Part 2 we added generation for Forests. In this post we will add Lakes.
The map with several lakes looks like this:

There are different ways to render the lakes. A lake could occupy one cell, or have asymmetric view like this:
G G G G G
G L L L G
G L L G G
But such morphs require a lot of different types of lakes to be created: a sprite when lake cell is surrounded by other lakes (in the middle). A sprite when left, diagonal and top sprites are also lakes...and so on.
For this game I will simplify lake shape to be 2x2 grid:
GGGGG
GLLGG
GLLGG
The lake can take only the shape of the square 2x2 size. To add it to map we need to:
This logic is covered in isRectangleFree method:
bool isRectangleFree(Point start, Point stop) {
// false if tile is not inside the map
try {
tileAtPoint(start);
tileAtPoint(stop);
} catch (e) {
return false;
}
for (var i = start.x; i <= stop.x; i++) {
for (var j = start.y; j <= stop.y; j++) {
var tile = tileAtPoint(Point(i, j));
if (tile.type == MAP_TILE_TYPES.GRASS) {
continue;
} else {
return false;
}
}
}
return true;
}
In try catch we use a side effect in tileAtPoint method: it will raise out of bounds exception if the point is outside the Map. Two for loops check whether all tiles in that rectangle are free. If at least one is already occupied, then the Rectangle is occupied.
Widget toWidgetTile(BuildContext context) {
var rotate = 0;
if (inDirection == DIRECTION_TYPES.UP &&
outDirection == DIRECTION_TYPES.RIGHT) {
rotate = 0;
}
if (inDirection == DIRECTION_TYPES.LEFT &&
outDirection == DIRECTION_TYPES.DOWN) {
rotate = 1;
}
if (inDirection == DIRECTION_TYPES.UP &&
outDirection == DIRECTION_TYPES.LEFT) {
rotate = 2;
}
if (inDirection == DIRECTION_TYPES.RIGHT &&
outDirection == DIRECTION_TYPES.UP) {
rotate = 3;
}
return RotatedBox(
quarterTurns: rotate,
child: Image.asset(
"images/background/map/lake_tile.png",
fit: BoxFit.fitWidth,
),
);
}
A tile knows how to render itself for debugging purposes (toString) and how to render in Flutter widget (toWidgetTile).
We add addLake method. It takes 10 tries in order to find a spot for the lake. Then populates all four tiles with LakeMapTile instance. Each lake tile get in/out direction set in order to correctly rotate the lake sprite to make it look like the lake occupies the whole 2x2 grid. The rotation starts from 0x0 point and goes clockwise to the 1x0
void addLake() {
var tries = 10;
while (tries-- > 0) {
var point = _getRandomPoint();
if (!isRectangleFree(point, Point(point.x + 1, point.y + 1))) {
continue;
}
var lake1 = LakeMapTile(x: point.x, y: point.y)
..inDirection = DIRECTION_TYPES.UP
..outDirection = DIRECTION_TYPES.RIGHT;
setTile(lake1);
var lake2 = LakeMapTile(x: point.x + 1, y: point.y)
..inDirection = DIRECTION_TYPES.LEFT
..outDirection = DIRECTION_TYPES.DOWN;
setTile(lake2);
var lake3 = LakeMapTile(x: point.x + 1, y: point.y + 1)
..inDirection = DIRECTION_TYPES.UP
..outDirection = DIRECTION_TYPES.LEFT;
setTile(lake3);
var lake4 = LakeMapTile(x: point.x, y: point.y + 1)
..inDirection = DIRECTION_TYPES.RIGHT
..outDirection = DIRECTION_TYPES.UP;
setTile(lake4);
break;
}
}
Debugging the generated Map 20x20 is quite cumbersome and we need to make sure that the lakes can be generated correctly. To verify that the logic works correctly we need to add unit tests:
test("Empty map has all rectangles free", () {
var map = WorldMap();
expect(map.isRectangleFree(Point(5, 5), Point(10, 10)), isTrue);
});
test(
"isRectangleFree returns false if at least one tile inside it is occupied.",
() {
var map = WorldMap();
map.setTileAtPoint(Point(8, 7), ForestMapTile());
expect(map.isRectangleFree(Point(5, 5), Point(10, 10)), isFalse);
});
test("isRectangleFree returns false for out of map bounds points", () {
var map = WorldMap(width: 10, height: 10);
expect(map.isRectangleFree(Point(-1, 5), Point(5, 5)), isFalse);
expect(map.isRectangleFree(Point(1, 5), Point(-5, 5)), isFalse);
expect(map.isRectangleFree(Point(1, 5), Point(15, 5)), isFalse);
expect(map.isRectangleFree(Point(15, 5), Point(5, 5)), isFalse);
});
test(
"isRectangleFree returns true if there is Occupied tile but outside the rectangle",
() {
var map = WorldMap();
map.setTileAtPoint(Point(8, 8), ForestMapTile(x: 8, y: 8));
expect(map.isRectangleFree(Point(5, 6), Point(7, 7)), isTrue);
});
void addLakes() {
8.timesRepeat(addLake);
}
final WorldMap map = WorldMap(width: 20, height: 20)
..addRiver()
..addForests()
..addLakes();
Map generated with Forest, River and Lakes looks like this:
