Continuous Terrain 2010 (a.k.a. Wanderland)

There are some old threads about Continuous Terrains for JME 1.0. Since I didn't see one that I liked, I wrote my own version.



By Continuous Terrain I mean: Where ever the player goes, he is surrounded by 9 "floor tiles". When ever he approaches the edge of the middle tile, the three tiles behind him detach, roll over, and attach themselves in front of him. This way the player never reaches the edge of the terrain, and you no longer need a wall or other artificial obstacles.



For example, the player starts out on the middle field (standing on 5), then walks east (6), and then south (9).

123    231    564
4X6 => 5X4 => 8X7
789    897    231



The example ("TerrainContinuous") that I saw here on the forum seems to work with only nine tiles, and it also contains a lot of  TerrainPage code.

My proposal ("Wanderland") works with any square number of floor tiles. You still only see 9 tiles at a time, but if you have more than 9 different ones, it will be less obvious that they are repeating. And with Wanderland, you can use anything you like as floor tiles! TerrainPages , TerrainBlocks, custom models, Boxes... Plaster the path with floating disks if it makes you happy!

How to use the Wanderland class?

  • Create a 2-d array of floor nodes. You must design the floors in a way that the edges match (my Class doesn't help with that). Apart from the floor, the floor nodes can also have buildings, trees, etc, attached to them.

  • Pass the array into the Wanderland constructor.

  • Attach the new wanderland object to the rootNode. (!)

  • Call wanderland.update(cam.getLocation()) in the update() loop.



Of course that only makes sense for certain types of games, since it is a bit illogical that you always return to where you started, the more you run away from it. ;) A Wanderland could be useful for first-person treassure hunting or chasing games where you just zigzag around.

Speaking of which: I haven't tested Wanderland together with physics or mobile agents. The problem is that mobiles may leave the tile to which they are attached, and that will cause some unsynchronized behaviour. So if anybody needs that, I can add a method that helps you to re-sync mobile objects and attach them to the floor tile they moved into.

I'll post the code in a minute, and a usage example. Please comment.

Lots of javadoc :wink:



package game.test;

import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import com.jme.scene.Node;
import com.jme.scene.Spatial;
import com.jme.scene.shape.Box;
import com.jme.scene.state.MaterialState;
import com.jme.scene.state.RenderState;
import com.jme.system.DisplaySystem;
import java.util.Random;

/**
 * A continuous floor for one player to wander about endlessly.
 * <br />
 * You provide the floor data in a square array of n*n fields. Each field
 * contains one <em>field node</em> that has a <em>floor spatial</em>
 * (and possibly other objects, trees, houses) attached to it.
 * <br />
 * In the update loop, you call wanderland.update() to keep 3*3 fields
 * directly around the player.
 * When the player reaches "the edge", Wanderland rolls over to the other
 * side of the array, thus giving the appearance of an "endless" landscape.
 * <br />
 * <p><b>Floor Spatials:</b></p>
 * <ul><li>can be a TerrainPage, Box, TerrainBlock, Quad, custom model, etc...
 * </li><li>must be square shaped.
 * </li><li>must have the same <tt>size</tt>.
 * </li><li>The number of Floor Spatials must be a square number >= 3.
 * <blockquote>If you use TerrainPages as floors and have stretched them,
 * reflect that in the <tt>size</tt> value in the constructor:
 * <tt>size = heightmap_size * stretch_factor</tt>.</blockquote>
 * </li><li>Provide Floors Spatials with edges that match, otherwise expect holes.
 * </li></ul>
 * <p><b>Field Nodes:</b></p>
 * <ul><li>
 * Attach each Floor Spatial to one Field Node. Attach all trees,
 * buildings, etc, that belong onto this floor to the same Field Node.
 * </li><li>You have full accesss to your Field Nodes, and you can
 * still modify them (attachments, State changes) during the game.
 * </li><li>Do not attach your Field Nodes to anything; pass them into
 * the wanderland constructor, and attach the wanderland object
 * to (a node attached to) the rootNode.
 * </li></ul>
 */
public class Wanderland extends Node {
  /** Wanderland floor fields */
  private final Node[][] data;
  /** field length in world units */
  private final int size;
  /** This Wanderland is made up of n*n floor fields */
  private final int n;
  /** Loc of floor spatials can be in center or corner, needs adjustment */
  private final Boolean fieldsAreCentered;
  /** Remembering old value to determine whether player stepped into new field */
  private Coord playerCoord_previous = new Coord(-1,-1);
  private DisplaySystem display;

  /**
   * Creates a continuous floor for one player to wander about endlessly.
   * Attach wanderland to rootNode, and use it together with update() in update loop.
   * @param data A 2-D array of your floor field nodes.
   * Array must be square, and must be larger or equal to 3*3.
   * @param size Side length of one square floor field in world units.
   * Must correspond to size of actual nodes that you provided,
   * otherwise there will be overlaps or gaps.
   * @param fieldsAreCentered Whether the provided floor fields' locs are
   * centered (e.g. TerrainPage, Box => true), or
   * in a corner (e.g. TerrainBlock, Quad => false).
   */
  public Wanderland(Node[][] data, int size, Boolean fieldsAreCentered) {
    this.data = data;
    this.size = size;
    this.fieldsAreCentered = fieldsAreCentered;
    this.n = (int) data.length; // ignoring the remainder
  }

  /**
   * DEMO mode. Creates a continuous floor for one player to wander about endlessly.
   * This constructor generates a flat continuous floor filled with simple random objects.
   * Attach this to rootNode, and use it together with update().
   * @param number Number of floor fields to generate.
   * Must be a square number >= 3*3. E.g. 9, 16, 25, 36...
   * @param size Side length of floor fields to generate, in world units.
   * @param display The main game's display system (to generate colors).
   */
  public Wanderland(int number, int size, DisplaySystem display) {
    this.size = size;
    this.display = display;
    this.fieldsAreCentered = true; // default for Box floor fields.
    this.data = new Node[number][number];
    this.n = (int) Math.sqrt(number);  // ignoring the remainder
    for (int row = 0; row < n; row++) {
      for (int col = 0; col < n; col++) {
        this.data[col][row] = new Node("DemoNode-" + col + "-" + row);
        Box floor = new Box(
          "demo-"+col+"-"+row , Vector3f.ZERO, size/2 , 0.1f , size/2 );
        ColorRGBA gaudy = ColorRGBA.randomColor();
        setColor(floor, gaudy);
        this.data[col][row].attachChild(floor);
        this.data[col][row].attachChild(createRandomContent(gaudy));
      }
    }
  }

  /**
   * Call wanderland.update(cam.getLocation()) in your game's update() loop!
   * @param playerloc3d The center of Wanderland in world units, e.g. cam.getLocation().
   */
  public void update(Vector3f playerloc3d) {
    // Multiplication factor for how far the player has wandered into new sectors.
    Coord wander = new Coord(
      (int) playerloc3d.x / (size * n) ,
      (int) playerloc3d.z / (size * n));
    // Make an adjustment to formula for negative numbers.
    if( playerloc3d.x < 0 ) wander.x -= 1;
    if( playerloc3d.z < 0 ) wander.z -= 1;
    // playerCoord is an x/z coordinate within the data[z][x] array.
    Coord playerCoord = new Coord(
      Math.abs((Math.abs((int) playerloc3d.x) - (Math.abs(wander.x) * size * n)) / size) ,
      Math.abs((Math.abs((int) playerloc3d.z) - (Math.abs(wander.z) * size * n)) / size) );
    // If player has stepped over into new field, recenter neighbouring fields:
    if ( !playerCoord_previous.equals(playerCoord) ) {
      this.detachAllChildren();
      for (int deltaX = -1; deltaX <= 1; deltaX++) {  
        for (int deltaZ = -1; deltaZ <= 1; deltaZ++) {
          attachField( playerCoord, wander, deltaX, deltaZ );
        }
      }
    }
    playerCoord_previous = playerCoord; // remember previous value
    this.updateGeometricState(0.1f, true);
    this.updateRenderState();
  }

  /** Attach the 9 neighbouring fields around the player.
   * @param playerCoord The player's coordinates within data[][].
   * @param playerWander In which sector the player has wandered.
   * @param deltaX Is it a neighbour to the left or right? -1|0|+1
   * @param deltaZ Is it a neighbour in front or behind?   -1|0|+1
   * @return Side effect: The node is attached and translated.
   */
  private void attachField( Coord playerCoord, Coord playerWander , int deltaX, int deltaZ ) {
    Coord4 neighbour = rollover(playerCoord, playerWander, deltaX, deltaZ);
    this.attachChild( data[neighbour.z][neighbour.x] );
    float neighbourLocX = ( neighbour.x * size + (neighbour.wx * size * n) );
    float neighbourLocZ = ( neighbour.z * size + (neighbour.wz * size * n) );
    if( fieldsAreCentered ) {   
      neighbourLocX += size / 2;
      neighbourLocZ += size / 2;
    }
    data[neighbour.z][neighbour.x].setLocalTranslation( neighbourLocX, 0, neighbourLocZ );
  }

  /**
   * Determine whether this neighbour needs a roll-over.
   * Give it the player coords and a delta (-1|0|+1),
   * and it returns coords and sector of the respective neighbouring field.
   * (Note: The playercoord is treated as a neighbour with delta 0|0.)
   * @return Four integers: The x/z values of the neighbouring field
   * inside data[][], and the x/z values of sector it is currently in.
   */
  private Coord4 rollover( Coord playerCoord, Coord playerWander, int deltaX, int deltaZ) {
    Coord neighbourCoord   = new Coord( playerCoord.x,  playerCoord.z  );
    Coord neighbourWander = new Coord( playerWander.x, playerWander.z );
    neighbourCoord.x = playerCoord.x + deltaX;
    neighbourCoord.z = playerCoord.z + deltaZ;
    // coord can roll over to other side of data[n][n] block.
    // wander can roll over into next sector.
    if  (neighbourCoord.x >= n) {
       neighbourCoord.x   -= n;
       neighbourWander.x  += 1;
    } else if (neighbourCoord.x < 0) {
       neighbourCoord.x   += n;
       neighbourWander.x  -= 1;
    }
    if (neighbourCoord.z  >= n) {
       neighbourCoord.z   -= n;
       neighbourWander.z  += 1;
    } else if (neighbourCoord.z < 0) {
       neighbourCoord.z   += n;
       neighbourWander.z  -= 1;
    }
    return new Coord4(neighbourCoord, neighbourWander);
  }

  /** =============== Structs ============== **/

  /** A struct that holds an int tupel, e.g. x/z coordinates. */
  private class Coord {
      public int x;
      public int z;
      public Coord(int x, int y) { this.x = x; this.z = y; }
      @Override
      public String toString() { return + x + "/" + z; }
      @Override
      public boolean equals(Object obj) {
       if (obj instanceof Coord) {
         Coord pt = (Coord)obj;
         return (x == pt.x) && (z == pt.z);
       }
       return super.equals(obj);
      }
      @Override
      public int hashCode() {
        int hash = 7;
        hash = 31 * hash + this.x;
        hash = 31 * hash + this.z;
        return hash;
      }
    }

  /** A struct that holds an int quadrupel, e.g. x/z/wx/wz coordinates. */
  private class Coord4 {
      public int x;
      public int z;
      public int wx;
      public int wz;
      public Coord4(int x, int z, int wx, int wz)
      { this.x = x; this.z = z; this.wx = wx; this.wz = wz; }
      public Coord4(Coord a, Coord b)
      { this.x = a.x; this.z = a.z; this.wx = b.x; this.wz = b.z; }
    }


  /** =============== Auxiliary methods for demos and testing ============== **/

  /** Auxiliary coloring method. */
  private void setColor(Spatial spatial, ColorRGBA color) {
    final MaterialState materialState = display.getRenderer().createMaterialState();
    materialState.setDiffuse(color);
    spatial.setRenderState(materialState);
  }

  /** Auxiliary method, only used with the DEMO constructor.
   * Create a few random box nodes to attach to the terrain
   */
  private Node createRandomContent(ColorRGBA c) {
    Node stuff = new Node("sample node");
    Random r = new java.util.Random();
    int max_num = r.nextInt(4) + 1;
    for (int i = 0; i < max_num; i++) {
      int scale1 = r.nextInt(size / 10) + 5;
      int scale2 = r.nextInt(size / 10) + 5;
      int scale3 = r.nextInt(size / 10) + 5;
      int loc = r.nextInt(size / 3);
      int pm1 = r.nextInt(2) == 1 ? 1 : -1;
      int pm2 = r.nextInt(2) == 1 ? 1 : -1;
      Box building = new Box("some stuff",
        new Vector3f(loc * pm1, scale2, loc * pm2),
        size / scale1, scale2, size / scale3);
      setColor(building, c);
      stuff.attachChild(building);
    }
    return stuff;
  }

}

Minimal demo: This constructor generates random colored flat Boxes as floor tiles.



In Wanderland(9, 300, display), replace the 9 with 16,25,36, etc to get less repetition and more tiles. Increase 300 to make the tiles wider (in a real game, one floor tile would be 500 world units or more, I guess).



public class HelloWanderland extends SimpleGame {

  private Wanderland wanderland;

  public static void main(String[] args) {
    HelloWanderland app = new HelloWanderland();    // Create Object
    app.setConfigShowMode(ConfigShowMode.ShowIfNoConfig);
    app.start();
  }

  protected void simpleInitGame() {
    cam.setLocation(new Vector3f(0f, 5f, 0f));
    wanderland = new Wanderland(9, 300, display); // 9,16,25,36

1 Like

I did not setup a project from it yet but this looks nice. I suppose using physics with this should be no problem, since you seem to move the tiles and not the player when he reaches the border. So the collision shape of the (static physics) floor and objects should be moved as well and the car or player or whatever moves on :slight_smile:

Of course the "original" continuous terrain for jME (jME Terra) features unlimited terrain (not just 9 "patches") and includes a plugin system for loading (and saving!) the height-map data, and some other features, like LOD, Threaded loading of data. And none of TerrainPage (or TerrainBlock) code if you consider that a feature.



Some other good work was done for it by other jME members, such as texture splashing, and jME2 support.



Apparently the source code (author yours truly) is a little hard to get into though :slight_smile:

llama said:

Apparently the source code (author yours truly) is a little hard to get into though :)

..and the LGPL license is a little hard to get over ;) but apparently its a great system, never tried it though.

@llama:



That's the right spirit! :smiley:



Sorry I never looked at Terra, from your description is seems to be a whole terrain system? Wanderland is "just" a helper class that shifts existing tiles around (That makes it relatively easy to update Wanderland to jme3).



Of course it would be best if we could just give the constructor one huge heightmap, and that would be split up into tiles and turned into a continuous terrain by the class, can Terra do that? I'm not certain I understood the details. How many tiles are visible at a time in Terra, also nine (out of the unlimited number)? I have no idea how much work it would be to integrate Terra into jme3, do you want to give it a try, llama? It's something we could need. :slight_smile:



@normen Yes, combining it with physics would sure work with one player. But you will feel tempted to add an enemy, or moving obstacles, or… landslides and soccerballs :wink: and then you'll realize that all these nice things will unceremoniously fall off the edge of Wanderland… (The way it is now.) :o



@all Uum… license. I say BSD. (My favorite license for code snippets is the DWTFYW, do WTF you want) :wink:



     

zathras said:

@llama:

That's the right spirit! :D

Sorry I never looked at Terra, from your description is seems to be a whole terrain system? Wanderland is "just" a helper class that shifts existing tiles around (That makes it relatively easy to update Wanderland to jme3).



Sometimes the simple and elegant systems are more useful than the big and complicated ones.

Yes, Terra is a complete system. Potentially you could use it to even load/unload object that are placed on the terrain for example.


Of course it would be best if we could just give the constructor one huge heightmap, and that would be split up into tiles and turned into a continuous terrain by the class, can Terra do that?


Yes, the source can be anything due to the plugin system. One implementation I did was where you could use the existing jME heightmap system, so that includes images, the random terrain generation system, or just a byte array / heightmap image. Then once you have that loaded, you could also save it to disk and in the future load it from disk.

You can still try the old compiled jME 1 demos here:

http://www.tijl.demon.nl/jme/terra/ (had to google that to find the page :P)

and reading the threads linked there to try and understand the system.

Somewhere there must be the google code project (edit, thanks google: http://code.google.com/p/jmeterra/ )with all the extension did by other people (if I remember right hevee and theprism?). Here you can check a project hevee did with it:

http://www.jmonkeyengine.com/forum/index.php?topic=5641.0

I was also told that is was used in this:
http://www.youtube.com/user/rherlitz#p/u/11/_CC7eFIzQi4


I'm not certain I understood the details. How many tiles are visible at a time in Terra, also nine (out of the unlimited number)? I have no idea how much work it would be to integrate Terra into jme3, do you want to give it a try, llama? It's something we could need. :)


To really understand it you have to read the threads linked from that little site. But basically there are heightmap tiles, and blocks (actual geometry, ie a scenegraph). As the player walks around in the background the system "scans" around first if it should load the heightmap, then (as you get closer) if it should decompress it, then if it should turn it into geometry (the geometry blocks can be smaller than the heightmaps though, for efficient culling and LODing), and finally for visible blocks what level of detail they should have. All this work is done in background task, using a custom task module I made.

Also it was designed to run well on an Athlon 800 with an NVidia TNT2 card (which it actually managed to do pretty well). For jME3 (which I just read requires OpenGL 2, so won't run on my Mac) you'd want to use GLSL at the PS3 level. With that you could just load a texture heightmap, and have shaders turn it into cool looking terrain. Also Java 5+ has it's own task system, my code was for Java 1.4.

Furthermore, I won't be making a terrain engine for jME3 any time soon since I'm not really involved in jME anymore. The only reason I saw this thread is become apparently I am still a moderator in this board :)

If I had to give anyone some advice for making one, start simple, don't try to do everything at once, and see what's needed by experimenting, not by trying to think of everything beforehand.
normen said:

llama said:

Apparently the source code (author yours truly) is a little hard to get into though :)

..and the LGPL license is a little hard to get over ;) but apparently its a great system, never tried it though.


Wanderland seems to lack any license though. Try getting that by your legal department ;)

I think the LGPL is mostly troublesome for people who don't understand it, since it does not hold you back from any commercial game development (1000ths of games use LGPL libraries). But as I stated at the time (I think), if terra ever got to a decent state there'd be no problem changing the license to something more liberal. Consider the LGPL license on it a form of self protection for people taking the code and making it even more of a mess :P

Really I can not blame anyone for not using it, since it's a bit of a mess and I never had time to finish it. Wanderland on the other hand seems pretty elegant and (shock) documented!

As I said, licenses can be changed. If you're serious about including it in jME, no problem, BSD it is. That was the idea from the start.


llama said:

Consider the LGPL license on it a form of self protection for people taking the code and making it even more of a mess :P

I can understand that completely, its just that jme3 still needs a good terrain engine or at least a basis for one (so its not so much of a "blank paper" job ;)) and sadly the LGPL in terra kicks it out of competition

Don't have a chance to load this up right now but I'll make sure to do some playing this week…  Unfortunately for our project it will be kind of tough to use this since I think the most we can load is about 100 sq.KM :frowning:

@Sbook: I'm not following: Wanderland is too slow/simple for your usecase, or it is overkill?

And speaking of "100 square kilometers", how much is that, how do we convert from world units to km?



I haven't done any performance testing with Wanderland, but I'd be interested what can be measured. I already threw 36 TerrainPages at it, and it worked just the same, but that's not a real crash test I guess?



Presently Wanderland creates all the terrains and nodes right away and keeps them in memory, which (in the worst case)  wastes memory and takes a while at start-up. When the player steps over an edge, it detaches ALL floor nodes and immediately reattaches nine. There is no optimization or "heuristics". :wink: There is no change in LOD because remote floors are detached anyway.



What is more efficient in jme: detachAll or a loop with lots of coordinate checking that detaches individual nodes?

You could rework a lot of my old code. Nothing you're doing is different than mine.

zathras said:

@Sbook: I'm not following: Wanderland is too slow/simple for your usecase, or it is overkill?
And speaking of "100 square kilometers", how much is that, how do we convert from world units to km?


Ah ok, I may have misinterpreted your idea.  It sounded like you only had 9 terrain blocks, but since you just mentioned 36, it sounds like you're thinking in terms of a bigger scope.

The problem for us would have been that having too few terrain blocks means very large world coordinates (Vector3f's in the millions), which causes problems in how the engine functions.  To combat this, the scene is scaled and partitioned. 1m = .001f and each zone in the Universal Transverse Mercator projection has it's own origin.

I'm definitely interested though, as we'll have the ultimate performance test coming up for it in a few months I think.  I've currently got a few hundred GB's of GeoTIFF terrain data sent to us by a subset of NASA that I'm going to start working with in the next month or two.   :lol:

zathras said:
Presently Wanderland creates all the terrains and nodes right away and keeps them in memory, which (in the worst case)  wastes memory and takes a while at start-up. When the player steps over an edge, it detaches ALL floor nodes and immediately reattaches nine. There is no optimization or "heuristics". ;) There is no change in LOD because remote floors are detached anyway.


That part is where I start to perk my ears up, the bit about only loading 100 Kilometers at a time is an issue of memory space :D 

zathras said:
What is more efficient in jme: detachAll or a loop with lots of coordinate checking that detaches individual nodes?


detatchAll should be more efficient since it doesn't do any coordinate checking..
normen said:

I did not setup a project from it yet but this looks nice. I suppose using physics with this should be no problem, since you seem to move the tiles and not the player when he reaches the border. So the collision shape of the (static physics) floor and objects should be moved as well and the car or player or whatever moves on :)


No No No, you will need to move the player too as floating points get rather inaccurate the larger they are.
theprism said:

No No No, you will need to move the player too as floating points get rather inaccurate the larger they are.

Yeah, that accounts for jme as well, so the approach for real infinite terrains would require some partitioning and location shifting anyway.

Sad but true, means you also have to hack your own input handling too or implement a move listener system to know when to shift the world…  Then again its probably cleaner to not use the jme input handlers.


hi, i found your code have a problem, a little error. the first constructor the n is wrong. it will make the wandlerland chid is out of index, you shuld make the n same with your sample logic.

1 Like

That thread is 6 years old by the way, so I doubt the guy will see/fix/need to still fix it. :smiley:
But thanks for the input though.

1 Like