[SOLVED] 2D Array swapping dimensions / PCG tilemap issues


  • administrators

    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 Bools, where true means solid and false 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 these HasWallAt() calls, X means Y and Y means X:

    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)

    tile issues

    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.


  • administrators

    Good idea. However, traceing out if we set the data more than once returns nothing. Which means they are indeed being processed only once.


  • administrators

    Alright, @sasik on the HaxeFlixel Discord resolved this. I was storing my tiles in MapData[x][y], whereas FlxTilemap.loadFrom2dArray() expects MapData[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


Log in to reply
 

Looks like your connection to HaxeFlixel was lost, please wait while we try to reconnect.