Lots of javadoc 
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;
}
}