The way minecraft does it is like this:
[java]
private List<Vector2f> getSurrounding(Vector2f chunkLoc)
{
// view distance var = amount of chunks we want to display in all directions.
// for example, a view distance of 3 would display a 7x7 area (3 chunks in all directions + the chunk you are standing in).
int topLx = toInt(chunkLoc.getX()) - viewDistance;
int topLz = toInt(chunkLoc.getY()) - viewDistance;
int botLx = toInt(chunkLoc.getX()) + viewDistance;
int botLz = toInt(chunkLoc.getY()) + viewDistance;
List<Vector2f> results = new ArrayList<Vector2f>();
for (int x = topLx; x <= botLx; x++)
{
for (int z = topLz; z <= botLz; z++)
{
results.add(new Vector2f(x, z));
}
}
return results;
}
[/java]
That method simply gives you the chunk locations around your player. Using that, you can work out the following:
[java]
// use the method above to calculate the chunks surrounding the player
// the “newChunkLoc” var is a Vector3f containing the chunk location of the player
// the “loadedChunk” var is a Vector2f ArrayList containing all loaded chunk co-ordinates.
List<Vector2f> surroundingChunkLocs = getSurrounding(newChunkLoc);
List<Vector2f> oldChunks = new ArrayList<Vector2f>(loadedChunks);
oldChunks.removeAll(surroundingChunkLocs);
List<Vector2f> newChunks = new ArrayList<Vector2f>(surroundingChunkLocs);
newChunks.removeAll(loadedChunks);
// if there are no new chunks, its because the loadedChunks arraylist was empty
// meaning the player just joined the game.
if (newChunks.isEmpty())
{
newChunks = surroundingChunkLocs;
}
[/java]
So now we have a list of the old chunks that we want to remove from the scene and a list of new chunks we want to add to the scene.
In the update method of the world or player, you would calculate whether the player had moved into a new chunk.
You can do this using bitshifting:
[java]
private int bitCalc(int blockSize)
{
switch (blockSize)
{
case 17: return 4;
case 33: return 5;
case 65: return 6;
case 129: return 7;
case 257: return 8;
case 513: return 9;
case 1024: return 10;
default: return 0;
}
}
int bitshiftVal = bitCalc(myBlockSize);
public static Vector2f WorldLocationToChunkLocation(Vector2f worldLocation)
{
int worldX = Math.round(worldLocation.getX());
int worldZ = Math.round(worldLocation.getY());
int chunkX = worldX >> bitshiftVal;
int chunkZ = worldZ >> bitshiftVal;
return new Vector2f(chunkX, chunkZ);
}
[/java]
So now, you can calculate if the player moved into a new chunk, and if so, which chunks you need to remove, and which chunks you need to load.
Below is a rough implementation of the above methods, using callables for good ol’ multi-threading.
[java]
public class TerrainGenerator extends TerrainQuad
{
private final GameContext context;
private final int viewDistance;
private final int bitshift;
private final Vector3f localScale;
private final List<Vector2f> loadedChunks;
private final HeightMapGenerator heightmapGen;
private final HumidityGenerator humidityGen;
private final TemperatureGenerator temperatureGen;
private final Plants trees;
public TerrainGenerator(GameContext context)
{
this.context = context;
this.loadedChunks = new CopyOnWriteArrayList<Vector2f>();
this.viewDistance = 4;
this.bitshift = bitCalc(Game.BLOCK_SIZE);
this.localScale = new Vector3f(1f, 1f, 1f);
this.trees = new Plants(context);
this.heightmapGen = new HeightMapGenerator();
this.humidityGen = new HumidityGenerator();
this.temperatureGen = new TemperatureGenerator();
this.lastChunkLoc = new Vector2f(Float.MAX_VALUE, Float.MAX_VALUE);
}
private int bitCalc(int blockSize)
{
switch (blockSize)
{
case 17: return 4;
case 33: return 5;
case 65: return 6;
case 129: return 7;
case 257: return 8;
case 513: return 9;
case 1024: return 10;
default: return 0;
}
}
private void loadVehicle()
{
// wait until we have some terrain before loading the car.
if (loadedChunks.isEmpty() == false && context.getVehicle() == null)
{
Vehicle vehicle =
// new Ford_Bronco(context);
new Lincoln_Navigator(context);
context.setVehicle(vehicle);
context.getGame().getRootNode().attachChild(vehicle.getVehicleNode());
vehicle.getVehicleControl().setPhysicsLocation(new Vector3f(5, 97, -15));
context.getGame().getPhysicsSpace().add(vehicle.getVehicleControl());
vehicle.initLights();
vehicle.setVehicleParked(true);
}
}
private void loadPlayer()
{
if (loadedChunks.isEmpty() == false && context.getPlayer() == null)
{
Player player = new Player(context);
context.setPlayer(player);
context.getGame().getRootNode().attachChild(player.getPlayerNode());
player.getCharacterControl().setPhysicsLocation(new Vector3f(0, 92, 0));
context.getGame().getPhysicsSpace().add(player.getCharacterControl());
player.registerActionListener();
context.getGame().setupCamera();
}
}
public void simpleUpdate(float tpf)
{
float actualX;
float actualZ;
// float actualX = this.context.getVehicle().getVehicleNode().getLocalTranslation().getX() + Game.TILE_SIZE;
// float actualZ = this.context.getVehicle().getVehicleNode().getLocalTranslation().getZ() + Game.TILE_SIZE;
actualX = this.context.getGame().getCamera().getLocation().getX() + Game.TILE_SIZE;
actualZ = this.context.getGame().getCamera().getLocation().getZ() + Game.TILE_SIZE;
pointX = toChunkCoord(actualX);
pointZ = toChunkCoord(actualZ);
Vector2f chunk = new Vector2f(pointX, pointZ);
// if player is in the same location, ignore
if (chunk.equals(lastChunkLoc) == false)
{
newChunkLoc = chunk.clone();
// new chunks
if (newChunksFuture == null)
{
newChunksFuture = context.getGame().getExecutor().submit(loadNewTerrain);
}
else
{
if (newChunksFuture.isDone())
{
try
{
ChunkState newState = (ChunkState)newChunksFuture.get();
// remove old
for (int i = 0; i < newState.getOldChunks().size(); i++)
{
Vector2f thisChunkLoc = newState.getOldChunks().get(i);
int chunkX = toLocationCoord(thisChunkLoc.getX());
int chunkZ = toLocationCoord(thisChunkLoc.getY());
String friendlyName = "chunk" + "_" + chunkX + "_" + chunkZ;
final TerrainQuad quad = (TerrainQuad)context.getGame().getRootNode().getChild(friendlyName);
context.getGame().getRootNode().detachChild(quad);
context.getGame().getPhysicsSpace().remove(quad);
loadedChunks.remove(thisChunkLoc);
}
// add new
for (int i = 0; i < newState.getNewChunks().size(); i++)
{
Chunk cnk = newState.getNewChunks().get(i);
context.getGame().getRootNode().attachChild(cnk);
context.getGame().getPhysicsSpace().add(cnk);
cnk.setBatchHint(BatchHint.Always);
// grass(cnk);
}
loadPlayer();
loadVehicle();
// finally
lastChunkLoc = newChunkLoc.clone();
newChunksFuture = null;
}
catch (Exception ex) { }
}
}
}
}
private int pointX;
private int pointZ;
private Vector2f newChunkLoc = null;
private Vector2f lastChunkLoc = null;
// private List<Vector2f> newChunks = new ArrayList<Vector2f>();
private Future newChunksFuture;
// private Future oldChunksFuture;
private Callable<ChunkState> loadNewTerrain = new Callable<ChunkState>()
{
public ChunkState call()
{
List<Chunk> chunksToAdd = new ArrayList<Chunk>();
List<Vector2f> surroundingChunkLocs = getSurrounding(newChunkLoc);
List<Vector2f> oldChunks = new ArrayList<Vector2f>(loadedChunks);
oldChunks.removeAll(surroundingChunkLocs);
List<Vector2f> newChunks = new ArrayList<Vector2f>(surroundingChunkLocs);
newChunks.removeAll(loadedChunks);
if (newChunks.isEmpty())
{
newChunks = surroundingChunkLocs;
}
for (int i = 0; i < newChunks.size(); i++)
{
Vector2f thisChunkLoc = newChunks.get(i);
int chunkX = ((Number)thisChunkLoc.getX()).intValue() << bitshift;
int chunkZ = ((Number)thisChunkLoc.getY()).intValue() << bitshift;
String friendlyName = "chunk" + "_" + chunkX + "_" + chunkZ;
loadedChunks.add(thisChunkLoc);
int imgChunkX = Math.round(thisChunkLoc.getX());
int imgChunkZ = Math.round(thisChunkLoc.getY());
float[] heightMap = heightmapGen.get(imgChunkX, imgChunkZ);
// float[] humidityMap = humidityGen.get(imgChunkX, imgChunkZ);
float[] temperatureMap = temperatureGen.get(imgChunkX, imgChunkZ);
// post processor for biomes
// temperature map affects textures
//
// float[] newHeightMap = Game.game.sandGenerator.process(imgChunkX, imgChunkZ, temperatureMap, heightMap);
Chunk quad = new Chunk(friendlyName, Game.TILE_SIZE, Game.BLOCK_SIZE, heightMap, temperatureMap, imgChunkX, imgChunkZ);
Image alphaImg = quad.getAlphaMap();
Texture alphaTex = new Texture2D(alphaImg);
// ByteBuffer pos_y = alphaImg.getData(2);
// ByteBuffer p2 = alphaImg.getData(3);
// alphaImg.setData(2, alphaImg.getData(3));
// alphaImg.setData(3, pos_y);
Material terrainMat = new Material(context.getGame().getAssetManager(), "Common/MatDefs/Terrain/TerrainLighting.j3md");
terrainMat.setBoolean("WardIso", false);
terrainMat.setFloat("Shininess", 0.2f);
terrainMat.setTexture("AlphaMap", alphaTex);
// TextureKey tk = new TextureKey("Textures/Terrain/Desert.jpg", false);
// load grass texture
Texture grass = context.getGame().getAssetManager().loadTexture("Textures/Terrain/Shrubland.jpg");
grass.setWrap(WrapMode.Repeat);
terrainMat.setTexture("DiffuseMap", grass);
terrainMat.setFloat("DiffuseMap_0_scale", 8f);
// load dirt texture
Texture dirt = context.getGame().getAssetManager().loadTexture("Textures/Terrain/Tundra.jpg");
dirt.setWrap(WrapMode.Repeat);
terrainMat.setTexture("DiffuseMap_1", dirt);
terrainMat.setFloat("DiffuseMap_1_scale", 8f);
// load rock texture
Texture rock = context.getGame().getAssetManager().loadTexture("Textures/Terrain/Snow.jpg");
rock.setWrap(WrapMode.Repeat);
terrainMat.setTexture("DiffuseMap_2", rock);
terrainMat.setFloat("DiffuseMap_2_scale", 8f);
quad.setMaterial(terrainMat);
quad.setLocalTranslation(chunkX, 0, chunkZ);
quad.addControl(new RigidBodyControl(new HeightfieldCollisionShape(quad.getHeightMap(), localScale), 0));
TerrainLodControl control = new TerrainLodControl(quad, context.getGame().getCamera());
control.setLodCalculator( new DistanceLodCalculator(Game.TILE_SIZE, 2.0f) ); // patch size, and a multiplier
quad.addControl(control);
quad.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
// trees
Random rnd = new Random();
for (int t = 0; t < rnd.nextInt(4); t++)
{
// genTree(quad);
}
for (int t = 0; t < rnd.nextInt(25); t++)
{
// genGrass(quad);
// genCloud(quad);
}
// grass
// Node grassNode = Grass.createPatchField(parent, context.getGame().getAssetManager(), "Textures/Terrain/Grass.jpg", 1.0f, 10f, 1.0f, 1.0f, 30f, 3.0f, 1.0f, 1, 4);
// quad.attachChild(grassNode);
chunksToAdd.add(quad);
}
return new ChunkState(chunksToAdd, oldChunks);
}
};
public int toInt(float value) { return ((Number)value).intValue(); }
public int toChunkCoord(float axis) { return ((Number)axis).intValue() >> this.bitshift; }
public int toLocationCoord(float axis) { return ((Number)axis).intValue() << this.bitshift; }
private List<Vector2f> getSurrounding(Vector2f chunkLoc)
{
int topLx = toInt(chunkLoc.getX()) - viewDistance;
int topLz = toInt(chunkLoc.getY()) - viewDistance;
int botLx = toInt(chunkLoc.getX()) + viewDistance;
int botLz = toInt(chunkLoc.getY()) + viewDistance;
List<Vector2f> results = new ArrayList<Vector2f>();
for (int x = topLx; x <= botLx; x++)
{
for (int z = topLz; z <= botLz; z++)
{
results.add(new Vector2f(x, z));
}
}
return results;
}
}
[/java]
And finally, a chunkstate is just a tuple really. I just like things to be nice and orderly.
[java]
public final class ChunkState
{
private final List<Chunk> newChunks;
private final List<Vector2f> oldChunks;
public ChunkState(List<Chunk> newChunks, List<Vector2f> oldChunks)
{
this.newChunks = newChunks;
this.oldChunks = oldChunks;
}
public List<Chunk> getNewChunks() { return this.newChunks; }
public List<Vector2f> getOldChunks() { return this.oldChunks; }
}
[/java]
Hope that helps 