[SOLVED] 2D Array swapping dimensions / PCG tilemap issues
-
Hey there!
I'm having problems which make me question my sanity. They aren't directly related to HaxeFlixel, but I'm at a loss and I need help. Here we go...
The Problem
I use cellular automata to generate a cave shape, using
Bool
s, wheretrue
means solid andfalse
means empty. Here are the relevant parts from the class:// Generation Parameters public var Width:Int = 60; public var Height:Int = 60; public var SolidChance:Float = 0.395; private var deathLimit:Int = 3; private var birthLimit:Int = 4; // Data public var MapData:Array<Array<Bool>> = new Array<Array<Bool>>(); public var TilemapData:Array<Array<Int>> = new Array<Array<Int>>(); public function new() { // This fills the array with random data seedMap(); // This smooths out the generated caves for (i in 0...4) { MapData = simulate(); } // Finds rooms, connects them and removes unused chunks processMap(); // Generates the data for use with FlxTilemap generateTilemap(); } private function seedMap() { for (x in 0...Width) { if (MapData[x] == null) { MapData[x] = new Array<Bool>(); } for (y in 0...Height) { if (y == 0 || y == Height - 1 || x == 0 || x == Width - 1) { MapData[x][y] = true; } else { MapData[x][y] = FlxG.random.bool(SolidChance * 100); } } } private function simulate():Array<Array<Bool>> { var simulatedMap:Array<Array<Bool>> = MapData.copy(); for (x in 0...Width) { for (y in 0...Height) { var neighbours:Int = countAliveNeighbours(x, y); if (MapData[x][y]) { if (neighbours < deathLimit) { simulatedMap[x][y] = false; } else { simulatedMap[x][y] = true; } } else { if (neighbours > birthLimit) { simulatedMap[x][y] = true; } else { simulatedMap[x][y] = false; } } } } return simulatedMap; } private function processMap():Void { var wallRegions:Array<Array<Tile>> = getRegion(true); var wallTresholdSize:Int = 50; for (wallRegion in wallRegions) { if (wallRegion.length < wallTresholdSize) { for (tile in wallRegion) { MapData[tile.X][tile.Y] = false; } } } var roomRegions:Array<Array<Tile>> = getRegion(false); var roomTresholdSize:Int = 50; var survingRooms:Array<Room> = new Array<Room>(); for (roomRegion in roomRegions) { if (roomRegion.length < roomTresholdSize) { for (tile in roomRegion) { MapData[tile.X][tile.Y] = true; } } else { survingRooms.push(new Room(roomRegion, MapData)); } } survingRooms.sort(function (roomA:Room, roomB:Room):Int { if (roomA.RoomSize > roomB.RoomSize) { return -1; } else if (roomA.RoomSize < roomB.RoomSize) { return 1; } return 0; }); survingRooms[0].IsMainRoom = true; survingRooms[0].IsAccessibleFromMainRoom = true; ConnectClosestRooms(survingRooms); } private function generateTilemap():Void { for (x in 0...Width) { if (TilemapData[x] == null) { TilemapData[x] = new Array<Int>(); } for (y in 0...Height) { if (MapData[x][y]) { TilemapData[x][y] = 15; if (HasWallAt(x - 1, y) && HasWallAt(x + 1, y) && HasWallAt(x, y - 1) && HasWallAt(x, y + 1)) { // we're walled off TilemapData[x][y] = 6; } if (!HasWallAt(x - 1, y) && HasWallAt(x + 1, y) && HasWallAt(x, y - 1) && HasWallAt(x, y + 1)) { // Left to us is empty, rest is filled TilemapData[x][y] = 5; } if (!HasWallAt(x + 1, y) && HasWallAt(x - 1, y) && HasWallAt(x, y - 1) && HasWallAt(x, y + 1)) { // Right to us is empty, rest is filled TilemapData[x][y] = 7; } if (!HasWallAt(x, y - 1) && HasWallAt(x, y + 1) && HasWallAt(x - 1, y) && HasWallAt(x + 1, y)) { // Above us is empty, is filled TilemapData[x][y] = 2; } if (!HasWallAt(x, y + 1) && HasWallAt(x, y - 1) && HasWallAt(x - 1, y) && HasWallAt(x + 1, y)) { // Below us is empty TilemapData[x][y] = 10; } } else { TilemapData[x][y] = (FlxG.random.bool(5) ? 4 : 8); } } } } // Utility functions private function countAliveNeighbours(x:Int, y:Int):Int { var count:Int = 0; for (i in -1...2) { for (j in -1...2) { var neighbour_x:Int = x + i; var neighbour_y:Int = y + j; if (i == 0 && j == 0) { } else if (neighbour_x < 0 || neighbour_y < 0 || neighbour_x >= Width || neighbour_y >= Height) { count = count + 1; } else if (MapData[neighbour_x][neighbour_y]) { count = count + 1; } } } return count; } private function isInMapRange(x:Int, y:Int):Bool { return x >= 0 && x < Width && y >= 0 && y < Height; } public function HasWallAt(x:Int, y:Int):Bool { if (isInMapRange(x, y)) { return MapData[x][y]; } return true; } }
It goes through 4 steps:
- Seed the map with random values
- Simulate the cellular automata to create shapes & smooth it out
- Check for small walls or rooms and remove them, and connect the remaining rooms
- Generate the tilemap data.
This works well, until step 4. As far as I can tell, this line of code checks if the tile to the right is not a wall (
false
), and that the rest (top, left, and bottom) are walls (true
).if (!HasWallAt(x + 1, y) && HasWallAt(x - 1, y) && HasWallAt(x, y - 1) && HasWallAt(x, y + 1)) { // ... }
However, it does not. This
if
returns true only if the bottom one is not a wall (false
) and the rest are walls (true
). By going through and checking where all of them end up actually verifying where there's a wall or not, I found out that in theseHasWallAt()
calls,X
meansY
andY
meansX
:if (!HasWallAt(x - 1, y) && HasWallAt(x + 1, y) && HasWallAt(x, y - 1) && HasWallAt(x, y + 1)) { // This evaluates to true when: // The TOP tile is empty, and the rest are filled } if (!HasWallAt(x + 1, y) && HasWallAt(x - 1, y) && HasWallAt(x, y - 1) && HasWallAt(x, y + 1)) { // This evaluates to true when: // The BOTTOM tile is empty, and the rest are filled } if (!HasWallAt(x, y - 1) && HasWallAt(x, y + 1) && HasWallAt(x - 1, y) && HasWallAt(x + 1, y)) { // This evaluates to true when: // The LEFT tile is empty, and the rest are filled } if (!HasWallAt(x, y + 1) && HasWallAt(x, y - 1) && HasWallAt(x - 1, y) && HasWallAt(x + 1, y)) { // This evaluates to true when: // The RIGHT tile is empty, and the rest are filled }
You can see here how they aren't in the right place (ignore the X tiles, it's corners)
Here's my actual question: is it possible that the array dimensions get switched around?
-
I did similar stuff recently. My guess is that several conditions are triggered at once, and right tile is overwritten by wrong one in the same iteration.
-
Good idea. However,
trace
ing out if we set the data more than once returns nothing. Which means they are indeed being processed only once.
-
Alright, @sasik on the HaxeFlixel Discord resolved this. I was storing my tiles in
MapData[x][y]
, whereasFlxTilemap.loadFrom2dArray()
expectsMapData[y][x]
. Had I used non-square maps I would've probably caught that earlier! He also gave me great tips on using 1D vectors instead to gain performance.:D