Endless worlds and infinite space

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.

How to do it...

Dynamic world loading can be created with the following steps:

  1. Create a new class called EndlessWorldControl. It should extend AbstractControl and implement ActionListener.
  2. We need to add a couple of fields to it as well. First of all, we need to keep track of the application's camera and store it in a parameter called 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.
  3. The class implements ActionListener and will handle movements based on user input. To do this, add four Booleans as well: moveForward, moveBackward, moveLeft, and moveRight.
  4. In the 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;
  5. In the 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);
  6. Using this, call a method called 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);
  7. Now, add the 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);
    }
  8. Then, create an 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;
      }
    }
  9. The location of this tile will be used to decide which other tiles should be loaded. Pass this to two new methods: updateTiles and deleteTiles.
  10. First, we take a look at the 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.
  11. Check whether 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));
  12. We set location to be the delta distance from 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);
  13. Finally, attach g to the control's spatial and put g in the cachedTiles map with wantedLocation as the key.
  14. Now, for the deleteTiles method, it also takes a Vector2f parameter called newLocation as the input.
  15. Like the 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);
      }
    }
  16. When you're done, simply parse through the tilesToDelete list, remove the tile from cachedTiles, and detach it from Spatial.
  17. There is one more thing we need to do before leaving the class. In the 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.

  18. First of all, we need to disable flyCam with flyCam.setEnabled(false) and possibly move the camera to some distance from the ground.
  19. Then, create a 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.
  20. Finally, set up some keys to control the movement and add the 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");

How it works...

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.222.32.67