Hello all. I have been reinventing Fyrestone's terrain loader, and this is a fairly successful attempt.
How it works: This system works by creating tiles and then reusing them. There will always be 9 tiles in the game, and they'll be destroyed / recycled as you move throughout the world. Tile-Coordinates are coordinates like 0,2 which don't represent the player's position, but instead represent where that tile needs to be.
The "Tile" Class: The tile class extends node and should be directly attached to your root or terrain node. Tiles are created by simply passing the tile coordinates.
The "TileManager" Class: A simple Singleton class which allows you to edit settings like your heightmap size, your map scale, your height scale, a default map, and the directories where you store your maps. In addition, it has a method which return a vector3f for setting local translation when passed a tile-coordinate, and an array list of tile-coordinates that should be loaded when you're standing on a specific tile.
The "TileCoordinate" Class: Simply an x,y grouped into a class for ease-of-use.
The file-system: set the location of your maps in the TileManager class, then name your maps by coordinate names (i.e. "0,0.raw" and "1,0.raw"). When you're moving around, the Tiles will load the correct heightmap (or the default heightmap if you're moving into uncharted areas that don't have specific heightmaps) and move them to the correct location.
Known Bugs:
- The way the Tile-Manager works will not detect when you walk into the first set of negative numbers. You have to walk > 1 tile into negative numbers before the terrain system will realize you've changed tiles.
- No texture splatting has been implemented yet.
If you have any questions feel free to ask, any modifications/improvements are definitely welcome. Hope somebody finds this useful.
Tile.java:
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package fyrestoneterrain;
import com.gibbon.mfkarpg.terrain.splatting.TerrainPassNode;
import com.jme.image.Texture;
import com.jme.scene.Node;
import com.jme.scene.PassNode;
import com.jme.scene.state.TextureState;
import com.jme.util.TextureManager;
import com.jmex.terrain.TerrainBlock;
import com.jmex.terrain.util.RawHeightMap;
import java.util.ArrayList;
/**
*
* @author Tyler Trussell
*/
public class Tile extends Node
{
private int x, y;
private RawHeightMap myMap;
private TerrainBlock myBlock;
private PassNode splat;
public int getX()
{
return x;
}
public int getY()
{
return y;
}
public Tile(int x, int y)
{
this.x = x;
this.y = y;
if(TestFyrestoneTerrain.class.getClassLoader().getResource(TileManager.mapDirectory + x + "," + y + TileManager.mapExtension) == null)
{
myMap = new RawHeightMap(TestFyrestoneTerrain.class.getClassLoader().getResource(TileManager.mapDirectory + TileManager.defaultMap + TileManager.mapExtension), TileManager.mapSize + 1, TileManager.mapFormat, true);
}
else this.myMap = new RawHeightMap(TestFyrestoneTerrain.class.getClassLoader().getResource(TileManager.mapDirectory + x + "," + y + TileManager.mapExtension), TileManager.mapSize + 1, TileManager.mapFormat, true);
myBlock = new TerrainBlock(x + "," + y, myMap.getSize(), TileManager.scale, myMap.getHeightMap(), TileManager.getOriginFor(x, y), false);
attachChild(myBlock);
}
public void changeMap(int x, int y)
{
System.out.println("Tile changed from " + this.x + "," + this.y + " to " + x + "," + y);
this.x = x;
this.y = y;
if(TestFyrestoneTerrain.class.getClassLoader().getResource(TileManager.mapDirectory + x + "," + y + TileManager.mapExtension) == null)
{
myMap = new RawHeightMap(TestFyrestoneTerrain.class.getClassLoader().getResource(TileManager.mapDirectory + TileManager.defaultMap + TileManager.mapExtension), TileManager.mapSize + 1, TileManager.mapFormat, true);
}
else myMap = new RawHeightMap(TestFyrestoneTerrain.class.getClassLoader().getResource(TileManager.mapDirectory + x + "," + y + TileManager.mapExtension), TileManager.mapSize + 1, TileManager.mapFormat, true);
myMap.load();
myBlock.setHeightMap(myMap.getHeightMap());
myBlock.updateFromHeightMap();
myBlock.setLocalTranslation(TileManager.getOriginFor(x, y));
myBlock.updateRenderState();
}
}
TileManager.java
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package fyrestoneterrain;
import com.jme.math.Vector3f;
import com.jme.scene.state.AlphaState;
import com.jme.system.DisplaySystem;
import com.jmex.terrain.util.RawHeightMap;
import java.util.ArrayList;
/**
*
* @author Tyler Trussell
*/
public class TileManager
{
public static final String mapDirectory = "maps/tiles/";
public static final String defaultMap = "0";
public static final int mapFormat = RawHeightMap.FORMAT_16BITLE;
public static final String mapExtension = ".raw";
public static final int mapSize = 64;
public static final int mapScale = 10;
public static final float mapHeightScale = .0007f;
public static final int tileSize = mapSize * mapScale;
/* NOT YET IMPLEMENTED
public static final int layers = 4;
public static final String alphaDirectory = "maps/alphas/";
public static final String textureDirectory = "maps/textures/";
public static final String[] layerNames = {"layer1", "layer2", "layer3", "layer4" };
public static final int textureScale = 1;
*/
public static final Vector3f scale = new Vector3f(mapScale, mapHeightScale, mapScale);
public static final AlphaState forTextures = DisplaySystem.getDisplaySystem().getRenderer().createAlphaState();
public static final AlphaState forLights = DisplaySystem.getDisplaySystem().getRenderer().createAlphaState();
public static Vector3f getOriginFor(int x, int y)
{
int x_, y_, z_;
x_ = x * tileSize;
y_ = 0;
z_ = y * tileSize;
return new Vector3f(x_, y_, z_);
}
public static ArrayList<TileCoordinate> getRequiredTiles(int x, int y)
{
ArrayList<TileCoordinate> toReturn = new ArrayList();
toReturn.add(new TileCoordinate(x - 1, y - 1));
toReturn.add(new TileCoordinate(x - 1, y));
toReturn.add(new TileCoordinate(x - 1, y + 1));
toReturn.add(new TileCoordinate(x, y - 1));
toReturn.add(new TileCoordinate(x, y));
toReturn.add(new TileCoordinate(x, y + 1));
toReturn.add(new TileCoordinate(x + 1, y - 1));
toReturn.add(new TileCoordinate(x + 1, y));
toReturn.add(new TileCoordinate(x + 1, y + 1));
return toReturn;
}
public static AlphaState textureAlpha()
{
forTextures.setBlendEnabled(true);
forTextures.setSrcFunction(AlphaState.SB_SRC_ALPHA);
forTextures.setDstFunction(AlphaState.DB_ONE_MINUS_SRC_ALPHA);
forTextures.setTestEnabled(true);
forTextures.setTestFunction(AlphaState.TF_GREATER);
forTextures.setEnabled(true);
return forTextures;
}
public static AlphaState lightAlpha()
{
forTextures.setBlendEnabled(true);
forTextures.setSrcFunction(AlphaState.SB_DST_COLOR);
forTextures.setDstFunction(AlphaState.DB_SRC_COLOR);
forTextures.setTestEnabled(true);
forTextures.setTestFunction(AlphaState.TF_GREATER);
forTextures.setEnabled(true);
return forTextures;
}
}
TileCoordinate.java:
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package fyrestoneterrain;
/**
*
* @author Tyler Trussell
*/
public class TileCoordinate
{
private int x;
private int y;
public TileCoordinate(int x, int y)
{
this.x = x;
this.y = y;
}
public int getX()
{
return x;
}
public int getY()
{
return y;
}
@Override
public String toString()
{
return x + "," + y;
}
}
TestFyrestoneTerrain.java:
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package fyrestoneterrain;
import com.jme.app.SimpleGame;
import com.jme.light.DirectionalLight;
import com.jme.math.Vector3f;
import com.jme.renderer.ColorRGBA;
import java.util.ArrayList;
/**
*
* @author Tyler
*/
public class TestFyrestoneTerrain extends SimpleGame
{
private int x, y, tempx, tempy;
ArrayList<TileCoordinate> mustLoad;
ArrayList<Tile> notNeeded;
private Tile[] world;
public static void main(String[] args)
{
TestFyrestoneTerrain app = new TestFyrestoneTerrain();
app.setDialogBehaviour(ALWAYS_SHOW_PROPS_DIALOG);
app.start();
}
@Override
protected void simpleInitGame()
{
lightState.detachAll();
DirectionalLight dl = new DirectionalLight();
dl.setDirection(new Vector3f(.5f, -.5f, 0));
dl.setAmbient(ColorRGBA.white);
dl.setDiffuse(ColorRGBA.white);
dl.setEnabled(true);
lightState.attach(dl);
x = (int) cam.getLocation().x / TileManager.tileSize;
y = (int) cam.getLocation().z / TileManager.tileSize;
loadFirstTiles();
updateTiles();
}
@Override
protected void simpleUpdate()
{
updateTiles();
}
private void loadFirstTiles()
{
world = new Tile[9];
int index = 0;
for(int i=0; i<3; i++)
{
for(int n=0; n<3; n++)
{
System.out.println("Loaded tile in slot " + index);
world[index] = new Tile(i, n);
rootNode.attachChild(world[index]);
index += 1;
}
}
}
private void updateTiles()
{
tempx = (int) (cam.getLocation().x / TileManager.tileSize);
tempy = (int) (cam.getLocation().z / TileManager.tileSize);
if(tempx != x || tempy != y)
{
x = tempx;
y = tempy;
System.out.println("Tile change detected.");
System.out.println("New tile: " + x + "," + y);
//Make a list of all tiles that need to be rendered.
mustLoad = TileManager.getRequiredTiles(x, y);
notNeeded = new ArrayList();
//Add tiles that we don't need to a list.
for(int i=0; i<world.length; i++)
{
boolean kill = true;
for(int n=0; n<mustLoad.size(); n++)
{
if(world[i].getX() == mustLoad.get(n).getX() && world[i].getY() == mustLoad.get(n).getY())
kill = false;
}
if(kill)
notNeeded.add(world[i]);
}
//Remove already loaded tiles from the list.
for(int i=0; i<mustLoad.size(); i++)
{
boolean kill = false;
for(int n=0; n<world.length; n++)
{
if(mustLoad.get(i).getX() == world[n].getX() && mustLoad.get(i).getY() == world[n].getY())
{
kill = true;
}
}
if(kill)
{
mustLoad.remove(i);
i-=1;
}
}
for(int i=0; i<mustLoad.size(); i++)
{
notNeeded.get(i).changeMap(mustLoad.get(i).getX(), mustLoad.get(i).getY());
}
}
}
}