Continuing from a previous question regarding infinite game worlds and coordinate data structures I have decided to go with a Hashmap. However, since I am trying this on Android I quickly ran into problems.
Here is a recap of requirements:
- Psuedo-infinite 2D world of tiles. The world will be generated as explored in all 4 directions (2D) So we need something that goes to negative indexes.
- Random access will be required for adjacent tiles etc.
- Need to be able to retrieve tile from a specific coordinate like get(x,y)
So, here is a snip of code I used to create a hashmap of 600 * 600 tiles.
Class for each Map tile:
public class MapTile {
public int type;
public MapTile(int type) {
this.type = type;
}
public boolean is(int type) {
return (type==this.type);
}
}
Test code for generating a big map:
long startTime;
long lastTime;
map = new HashMap<Point, MapTile>();
int x;
int y;
int tile;
startTime = System.currentTimeMillis();
lastTime = startTime;
for (x=-300;x<=300;x++) {
for (y=-300;y<=300;y++) {
tile = (x+y)%2; //Just so I differentiate the tiles
this.map.put(new Point(x,y), new MapTile(tile));
}
// Logging each row
Log.d(this.getClass().toString(), "Generated x:" + x + " Time in ms:"+(System.currentTimeMillis() - lastTime));
lastTime = System.currentTimeMillis();
}
So, when I run similar code in Java. It completes successfully (eats a lot of memory though). No problem.
But, when I try to do this in Android on a top-end device, it practically halts before reaching x = 190 mark (usually). Garbage collector kicks in, and endless loop ensues:
** snip **
05-26 15:52:19.565 10758-10758/com.ayk.test04 D/class com.ayk.test04.WorldMap2D: Generated x:187 Time in ms:2
05-26 15:52:19.570 10758-10758/com.ayk.test04 D/class com.ayk.test04.WorldMap2D: Generated x:188 Time in ms:6
05-26 15:52:19.575 10758-10758/com.ayk.test04 D/class com.ayk.test04.WorldMap2D: Generated x:189 Time in ms:2
05-26 15:52:19.995 10758-10760/com.ayk.test04 I/dalvikvm-heap: Clamp target GC heap from 66.002MB to 64.000MB
05-26 15:52:19.995 10758-10760/com.ayk.test04 D/dalvikvm: GC_CONCURRENT freed 3K, 2% free 64797K/65543K, paused 4ms+30ms, total 429ms
05-26 15:52:19.995 10758-10758/com.ayk.test04 D/dalvikvm: WAIT_FOR_CONCURRENT_GC blocked 421ms
05-26 15:52:20.335 10758-10758/com.ayk.test04 I/dalvikvm-heap: Clamp target GC heap from 66.002MB to 64.000MB
05-26 15:52:20.335 10758-10758/com.ayk.test04 D/dalvikvm: GC_FOR_ALLOC freed 0K, 2% free 64797K/65543K, paused 341ms, total 341ms
05-26 15:52:20.335 10758-10758/com.ayk.test04 I/dalvikvm-heap: Clamp target GC heap from 64.002MB to 64.000MB
05-26 15:52:20.335 10758-10758/com.ayk.test04 I/dalvikvm-heap: Grow heap (frag case) to 64.000MB for 16-byte allocation
05-26 15:52:20.695 10758-10760/com.ayk.test04 I/dalvikvm-heap: Clamp target GC heap from 66.002MB to 64.000MB
05-26 15:52:20.695 10758-10760/com.ayk.test04 D/dalvikvm: GC_CONCURRENT freed 0K, 2% free 64797K/65543K, paused 12ms+2ms, total 358ms
05-26 15:52:20.695 10758-10758/com.ayk.test04 D/dalvikvm: WAIT_FOR_CONCURRENT_GC blocked 332ms
** thousands of these... snip **
So, pretty much, I am stuck here.
My tiles only contain an int variable for now. This is barebones. So I understand, it is technically impossible to create a Hashmap of 50.000 or so objects? This seems a bit small for me.
Things I want to try are:
- Only keeping a live region of the map in memory while offloading the rest to storage. While this may solve the memory issue, I am afraid it may cause performance problems while accessing tiles that are out of the live area.
- Somebody suggested using chunks (ala Minecraft). So I keep a hashmap of hashmaps? Would it work? If so, how do I do it?
- Am I completely off the target and looking at this the wrong way?
Thanks for any help you may bring in.
Update
Hello again. With your help, I have decided to go with using a database. It works much more efficiently and so far, I haven't run into any problems. For posterity, here is how it works:
The database
Working in Android, I have set up an SQLite database and table for storing each tile. At first, I decided to run an insert query each time a tile is generated. This quickly turned out to be another performance bottleneck because each INSERT took a bit over 2ms to complete, throwing a wrench into even the smallest 300x300 tile generation process. I had to utilize a multiple INSERT query which took many tiles and stored them using a single query. So, I have created an "tile buffer" where each tile is stored with their coordinates:
//A linked list of integers is good enough.
//The first 2 are X and Y coordinates, the third one is the tile type.
//I can add more fields if needed.
this.tileDBbuffer = new LinkedList<Integer[]>();
Here is how the buffer is filled:
void addTileBuffer(int x,int y, MapTile map_tile) {
int maxsize = 10000;
Integer[] tile_data;
tile_data = new Integer[3];
tile_data[0] = x;
tile_data[1] = y;
tile_data[2] = map_tile.type;
if (this.tileDBbuffer.size()>= maxsize) {
Log.d(this.getClass().toString(), "Buffer filled up... Saving");
savemapbuffer();
}
this.tileDBbuffer.add(tile_data);
}
Once the buffer reaches a certain size (or the tile generation ends) the "savemapbuffer()" function is ran, which basically calls this function first, and then purges the buffer:
public void addTileBulk(LinkedList<Integer[]> map_tiles) {
Integer[] tile_data = new Integer[3];
if (db == null) db = this.getWritableDatabase();
String sql = "INSERT INTO "+TABLE_NAME_MAP+" ("+COLUMN_NAME_MAP_X+", "+COLUMN_NAME_MAP_Y+", "+COLUMN_NAME_MAP_TILE_TYPE+") VALUES (?,?,?)";
db.beginTransaction();
SQLiteStatement stmt = db.compileStatement(sql);
try {
while (!map_tiles.isEmpty()) {
tile_data = map_tiles.pop();
stmt.bindLong(1,tile_data[0]);
stmt.bindLong(2,tile_data[1]);
stmt.bindLong(3,tile_data[2]);
stmt.execute();
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
map_tiles.clear();
}
Hashmap in use
Soon after this, problems occurred while drawing the map. The drawing is done in another thread, so iterating through it while simultaneously generating new tiles threw lots of weird errors. After testing for a bit using "is the map busy" booleans, I ended up using a ConcurrentHashmap instead of a regular Hashmap. It worked.
Chunks
After that, I ended up needing to use "chunks" after all. It was easier to manage discrete sections of the map rather than estimating which tiles to load based on player distance. So now, each HashMap element is a fixed 2 dimensional integer array, containing tiles. I call them Cells
map = new ConcurrentHashMap<Point, MapCell>();
and a cell is something like this:
public class MapCell {
public int x,y;
private static int cell_size = 32;
public MapTile[][] tiles = new MapTile[cell_size][cell_size];
}
Now. I am off to Perlin noise and map generation adventures :) See you next time.