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):
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):
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 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.
Answer
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.
No comments:
Post a Comment