3
\$\begingroup\$

I wrote this code to randomly generate a square terrain that somewhat resembles an island. First, I randomly populated a square with grass tiles (1s) on a plane of water tiles (0s):

enter image description here

Then, I looped through each tile and changed its tile type according to its neighboring tiles(if a tile is surrounded by 4 or more water tiles, it becomes a water tile, otherwise it becomes a grass tile):

enter image description here

The results don't seem all that pleasing; not only is the terrain filled with holes and lack overall "togetherness", it is also weirdly symmetrical diagonally.

Here's the code:

private void SmoothMap (Chunk chunk)
{
    for(int row = 1; row < chunk.rows-1; row++)
    {
        for(int col = 1; col < chunk.cols-1; col++)
        {
            //if current tile is not null
            if(chunk.GetTile(row, col) != null)
            {
                //Check its neighboring tiles
                int neighboringTiles = GetSurroundingTiles(row, col, chunk);
                //System.out.println("Position: " + row + ", " + col);
                //System.out.println("Current tile type: " + chunk.GetTile(row, col).type);
                //System.out.println("Neighboring water tiles: " + neighboringTiles);

                if (neighboringTiles > 4)
                {
                    if (chunk.GetTile(row, col) != null)
                    {
                        chunk.GetTile(row, col).texture = GetRandomWaterTexture();
                        chunk.GetTile(row, col).type = Enums.tileType.Water;
                    }
                }
                else if (neighboringTiles < 4)
                {
                    if (chunk.GetTile(row, col) != null)
                    {
                        chunk.GetTile(row, col).texture = GetRandomGrassTexture();
                        chunk.GetTile(row, col).type = Enums.tileType.Grass;
                    }
                }
            }
        }
    }
}

private int GetSurroundingTiles(int r, int c, Chunk chunk)
{
    int surroundingTiles = 0;

    for(int x = c-1; x <= c+1; x++)
    {
        for(int y = r-1; y <= r+1; y++)
        {
            if (x != c || y != r)
            {
                if(chunk.GetTile(x, y) != null)
                {
                    if (chunk.GetTileCode(x, y).equals("0"))
                    {
                        surroundingTiles++;
                    }
                }
            }
        }
    }
    return surroundingTiles;
}

Does anyone know why it is the way it is? Or a better way to approach generating an terrain like this?

Here's the GetTile() code:

public String GetTileCode(int r, int c)
{
    Tile tile;
    ArrayList<Tile> chunk_row;
    if(tiles.size() > r && r >= 0){
        chunk_row = tiles.get(r);

        if(chunk_row != null && chunk_row.size() > c && c >= 0){
            tile = chunk_row.get(c);
            return tile.isGrass() ? "1" : "0";
        }
    }
    return "0";
}

1 is grass, and 0 is water.

\$\endgroup\$
  • \$\begingroup\$ after calling the SmoothMap method 4 times, the terrain looked a bit better; however it is obviously symmetrical (something I want to avoid in a randomly generated terrain). \$\endgroup\$ – Harrylujah Feb 19 '18 at 5:18
  • \$\begingroup\$ Can we see your chunk.GetTileCode()? \$\endgroup\$ – Engineer Feb 19 '18 at 5:25
  • \$\begingroup\$ @ArcaneEngineer the question is now updated. \$\endgroup\$ – Harrylujah Feb 19 '18 at 5:30
3
\$\begingroup\$

You are writing to the same array you are reading from - that's what causes diagonal symmetry. Problem is you can't sample correctly if you have already begun to change the array you are sampling from! You must have a copy of the tiles array in order to avoid this. Write into a fresh/clear array, then copy contents over from it after you are finished smoothing. Here is code that works:

public struct Tile //struct allows copy. We need this for copying to and fro from tiles to tilesSmoothed. 
{
    public bool isLand;
    public string somethingElse;
}

public class Main : MonoBehaviour
{
    public const int ROWS = 50;
    public const int COLS = 50;

    Tile[,] tiles = new Tile[COLS, ROWS];
    Tile[,] tilesSmoothed = new Tile[COLS, ROWS];
    GameObject[,] tileGOs = new GameObject[COLS, ROWS]; 

    void Start ()
    {
        for (int x = 0; x < COLS; x++)
        {
            for (int y = 0; y < ROWS; y++)
            {
                //data model
                tiles[x,y] = new Tile();
                tiles[x,y].isLand = Random.Range(0.0f, 1.0f) >= 0.5f ? true : false;

                //tilesSmoothed[x,y] = new Tile(); //unnecessary - all structs are initialised to zero / false

                //view
                tileGOs[x,y] = GameObject.CreatePrimitive(PrimitiveType.Cube);
                tileGOs[x,y].transform.position = new Vector3(x, y, 0);
                tileGOs[x,y].SetActive(tiles[x,y].isLand);
            }
        }
    }

    void Update ()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            SmoothMap();
        }
    }

    private void SmoothMap()
    {
        //1st loop populates tilesSmoothed
        for (int x = 1; x < COLS-1; x++)
        {
            for (int y = 1; y < ROWS-1; y++)
            {
                //if current tile is not null
                if (tiles[x,y].isLand)
                {
                    //Check its neighboring tiles
                    int neighbourCount = CountNeighbourTiles(x, y);
                    tilesSmoothed[x,y].isLand = neighbourCount > 4;
                }
            }
        }

        //2nd loop copies those results from tilesSmoothed into tiles
        for (int x = 0; x < COLS; x++)
        {
            for (int y = 0; y < ROWS; y++)
            {
                //model
                tiles[x,y].isLand = tilesSmoothed[x,y].isLand; //COPY!
                //remember to also clear tilesSmoothed[x,y] here if you like, but for now this is not req'd.

                //view
                tileGOs[x,y].SetActive(tiles[x,y].isLand);
            }
        }
    }

    private int CountNeighbourTiles(int x, int y)
    {
        int count = 0;

        for (int nx = x-1; nx < x+1+1; nx++)
        {
            for (int ny = y-1; ny < y+1+1; ny++)
            {
                //if (x != c || y != r) //this is not required and is costly - see return line below
                //{
                if (tiles[nx,ny].isLand)
                {
                    count++;
                }
                //}
            }
        }
        return count - 1; //you can assume -1 since the current tile must be valid and was definitely counted
    }
}

As for your approach to smoothing...

SmoothMap should probably handle cases where e.g. we are surrounded by 3 tiles (checking horizontally and vertically, the count goes up to 4; checking diagonally, the number can go up to 8). You can either count the total surrounding valid tiles and act accordingly, or act on them according to structure, such as look at who is adjacent to who, e.g. 2 in the top right and 3 in the bottom left - neither group touching the other. None of this is essential to solving your main problem which is symmetry, but it will help with smoothing if implemented well. I've changed your GetSurroundingTiles to be subtly more efficient.

What you're doing here is a 3x3 box filter which is a sound and common approach. For a smoother map, you must increase the resolution of the grid, i.e. increase the values of ROWS & COLS.

As a lesson to take from this, remember always to reduce your code down to the most basic thing that works, in order to solve mind-numbing problems.

\$\endgroup\$
  • 1
    \$\begingroup\$ Thanks for the suggestion, I will post updates when I figure something out. \$\endgroup\$ – Harrylujah Feb 19 '18 at 5:56
  • \$\begingroup\$ @Harrylujah Feel free to post any further queries here. \$\endgroup\$ – Engineer Feb 19 '18 at 6:09
  • \$\begingroup\$ I cleaned up my coded a little bit and got rid of the ArrayList within an ArrayList implementation for storing tiles. I'm now using a more straightforward Tile[][] for simpler reading and writing tiles, but I'm still getting the exact same results... \$\endgroup\$ – Harrylujah Feb 19 '18 at 6:14
  • \$\begingroup\$ as for SmoothMap(), I loop through every tile excluding the edges to determine its neighboring edges, and I check every tile surrounding the current tile including diagonals... I'm not sure what you mean by the example you've given. \$\endgroup\$ – Harrylujah Feb 19 '18 at 6:21
  • \$\begingroup\$ @Harrylujah Have you begun logging out the indices? You have to do this to find the problem. First, do us a favour: Run SmoothMap once, then twice, then thrice, and tell me if it becomes more symmetrical each time. This is very important. I know at the moment you are doing it 4x. \$\endgroup\$ – Engineer Feb 19 '18 at 6:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.