There's really no such thing as endless or infinite in computer-generated worlds. Sooner or later, you're going to hit one limit or the other. However, there are some techniques that will get you further than others. The normal approach when creating a game is to move the player around the game world. Those who have tried to, for example, make a space exploration game in this way have noticed that pretty soon problems with regards to float numbers appear. This is because float values are not evenly spaced. As their values increase, their precision decreases. Using doubles rather than floats will only delay what's inevitable.
If you can't even have a solar system as a human-scaled game world, how can you then have a whole galaxy? As an old saying goes, "If Mohammed won't come to the mountain, the mountain must come to Mohammed." That is exactly the solution to our first problem! By making the game world move around the player, we ensure that the precision remains high. This is great for large-scale game worlds. The drawback is that it requires a different architecture. Switching how the game world is generated or loaded during the mid-development stage can be a huge task. It's better to decide this during the design phase.
Another problem is the sheer size of the worlds. You can't simply store all the terrain-based game world of a decent size in the memory at once. We can solve this problem by loading world data on demand and throwing it away when we don't need it any more. This recipe will use a simple method to generate the world on demand, but the principle can be applied to other methods, such as generating a heightmap or loading the world from a storage device.
Dynamic world loading can be created with the following steps:
EndlessWorldControl
. It should extend AbstractControl
and implement ActionListener
.cam
. The class also requires a Geometry
parameter called currentTile
to represent the currently centered game area. A Material
parameter called material
will be used on the geometries and a HashMap<Vector2f, Geometry>
parameter called cachedTiled
will store the entire currently active game world.ActionListener
and will handle movements based on user input. To do this, add four Booleans as well: moveForward
, moveBackward
, moveLeft
, and moveRight
.onAction
method, add the following code to set the Booleans based on the input:if (name.equals("Forward")) moveForward = isPressed; else if (name.equals("Back")) moveBackward = isPressed; else if (name.equals("Left")) moveLeft = isPressed; else if (name.equals("Right")) moveRight = isPressed;
controlUpdate
method, move the tiles based on the direction of the camera and the Booleans you just created. First, get the current forward direction of the camera and the direction which is to the left of it. Then, multiply it by tpf
to get an even movement and an arbitrary value to increase the speed of the movement as follows:Vector3f camDir = cam.getDirection().mult(tpf).multLocal(50); Vector3f camLeftDir = cam.getLeft().mult(tpf).multLocal(50);
moveTiles
if any movement should occur as follows:if(moveForward) moveTiles(camDir.negate()); else if (moveBackward) moveTiles(camDir); if(moveLeft) moveTiles(camLeftDir.negate()); else if (moveRight) moveTiles(camLeftDir);
moveTiles
method that takes a Vector3f
object called amount
as the input. First, parse through the values of the cachedTiles
map and apply the amount value as follows:for(Geometry g: cachedTiles.values()){ g.move(amount); }
Iterator
object and iterate through cachedTiles
again; stop if any of the tiles contain Vector3f.ZERO
, which is the location of the camera. This is our new currentTile
object. This can be implemented as follows:Vector2f newLocation = null; Iterator<Vector2f> it = cachedTiles.keySet().iterator(); while(it.hasNext() && newLocation == null){ Vector2f tileLocation = it.next(); Geometry g = cachedTiles.get(tileLocation); if(currentTile != g && g.getWorldBound().contains(Vector3f.ZERO.add(0, -15, 0))){ currentTile = g; newLocation = tileLocation; } }
updateTiles
and deleteTiles
.updateTiles
method. It takes a Vector2f
parameter called newLocation
as the input. Create a nested for
loop that goes from x-1
and y-1
to x+1
and y+1
.cachedTiles
already has the tile with newLocation
and x
and y
combined. If it doesn't, we create a new tile and apply BoundingBox
of the same size as the tile:Vector2f wantedLocation = newLocation.add(new Vector2f(x,y)); if(!cachedTiles.containsKey(wantedLocation)){ Geometry g = new Geometry(wantedLocation.x + ", " + wantedLocation.y, new Box(tileSize * 0.5f, 1, tileSize * 0.5f));
newLocation
. If currentTile
is not null, we add its localTranslation
too:Vector3f location = new Vector3f(x * tileSize, 0, y * tileSize); if(currentTile != null){ location.addLocal(currentTile.getLocalTranslation()); } g.setLocalTranslation(location);
g
to the control's spatial and put g
in the cachedTiles
map with wantedLocation
as the key.deleteTiles
method, it also takes a Vector2f
parameter called newLocation
as the input.updateTiles
method, iterate through the cachedTiles
map. Look for those tiles that are now more than two tiles away in either direction and add their location to a list called tilesToDelete
:Iterator<Vector2f> it = cachedTiles.keySet().iterator(); List<Vector2f> tilesToDelete = new ArrayList<Vector2f>(); while(it.hasNext()){ Vector2f tileLocation = it.next(); if(tileLocation.x>newLocation.x + 2 || tileLocation.x<newLocation.x - 2 || tileLocation.y>newLocation.y + 2 || tileLocation.y<newLocation.y - 2){ tilesToDelete.add(tileLocation); } }
tilesToDelete
list, remove the tile from cachedTiles
, and detach it from Spatial
.setSpatial
method, we should add a call to updateTiles
, supplying Vector2f.ZERO
to it to initialize the generation of the tile.For a larger implementation, we might want to introduce an AppState
instance to handle this, but here we will manage it with a test application.
flyCam
with flyCam.setEnabled(false)
and possibly move the camera to some distance from the ground.Node
class called worldNode
and an EndlessWorldControl
instance called worldControl
. Attach worldNode
to rootNode
and supply the worldControl
object with a material before adding it to worldNode
and setting the camera.worldControl
object as a listener; refer to the following code on how to do this:inputManager.addMapping("Forward", new KeyTrigger(KeyInput.KEY_UP)); inputManager.addMapping("Back", new KeyTrigger(KeyInput.KEY_DOWN)); inputManager.addMapping("Left", new KeyTrigger(KeyInput.KEY_LEFT)); inputManager.addMapping("Right", new KeyTrigger(KeyInput.KEY_RIGHT)); inputManager.addListener(worldControl, "Forward", "Back", "Left", "Right");
The process that we follow is that if a movement occurs, the moveTiles
method will first move all the tiles to cachedTiles
. It then checks to see whether there's a new tile that should be the center or whether it should be currentTile
. If this happens, other tiles must be checked to see which ones should be kept and which ones need to be generated. This happens in the updateTiles
method. Last in the chain is the deleteTiles
method that checks which tiles should be removed because they are too far away.
If we print out the translation of the tiles, we can see that they are never very far from the center of their parent node. This happens because when we generate the tiles, we place them relative to currentTile
. Since currentTile
is also based on a relative position, things never move very far. It's almost like a conveyor belt.
18.222.32.67