Hi all again. This summer I started working again on a Voxel Engine. Working with three-dimensional arrays, ArrayLists and quads (and a bit of GeometryBatchFactory.optimize() ) I could get over 200 FPS in full zones and 1500 in zones without many quads. Every block is based on a “Cell”: “abstract” object that holds and decides what faces of the cube have to be shown. When a Chunk is updates, every Cell in it is updated. This usually takes about 15 or less milliseconds, but, randomly, it can take over 1500 milliseconds, making the whole thing unplayable. The idea was to execute the update method in a parallel thread, so that it wouldn’t affect the main thread. After more than two weeks of tries, I couldn’t get something that works. Even if the update method is called from a Callable, submitted by an executor, it still affects the Main Thread, making the whole game freeze for random time (depending on how much time the update method takes). I post here my code, can you please help me? I’m actually getting mad because of this. Searching in the internet I found various examples, but no one worked.
(The Callable that executes the chunk update method is at the end of the class)
package mygame;
import com.jme3.app.Application;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.math.Vector3f;
import java.util.ArrayList;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import static mygame.Chunk.chunkSideDim;
public class WorldProvider extends AbstractAppState {
Main app;
Random rand = new Random();
private final static int maxX = 20, maxY = 20, maxZ = 20;
public static Chunk[][][] chunks = new Chunk[maxX][maxY][maxZ];
//public static Chunk[][][] chunks = new Chunk[maxX][maxY][maxZ];
boolean worldGenerated = false;
public static boolean firstCellPlaced = false;
int pX, pY, pZ; //player coords
PlayerControlState playerControl;
int prevX, prevY, prevZ;
boolean first = false;
int renderDistance = 6;
int updateX, updateY, updateZ;
public static ExecutorService executor = Executors.newSingleThreadExecutor();
@Override
public void initialize(AppStateManager stateManager, Application app) {
super.initialize(stateManager, app);
this.app = (Main) app;
playerControl = this.app.getStateManager().getState(PlayerControlState.class);
//int worldLimitX = maxX * chunkSideDim;
//int worldLimitZ = maxZ * chunkSideDim;
int worldLimitX = maxX * (renderDistance + 2);
int worldLimitZ = maxZ * (renderDistance + 2);
for (int i = 0; i < worldLimitX; i++) {
for (int j = 0; j < worldLimitZ; j++) {
setCell(i, 0, j, Id.GRASS);
}
}
for (int i = 0; i <= worldLimitX / 2; i++) {
createHill(rand.nextInt(worldLimitX), rand.nextInt(worldLimitZ), rand.nextInt(chunkSideDim), rand.nextInt(chunkSideDim), (rand.nextInt(maxX) + 1));
}
for (int i = 0; i < maxX; i++) {
for (int j = 0; j < maxY; j++) {
for (int k = 0; k < maxZ; k++) {
if (WorldProvider.chunks[i][j][k] != null) {
WorldProvider.chunks[i][j][k].update();
}
}
}
}
executor.submit(r);
}
@Override
public void update(float tpf) {
pX = playerControl.getX() / chunkSideDim;
pY = playerControl.getY() / chunkSideDim;
pZ = playerControl.getZ() / chunkSideDim;
for (int i = 0; i < maxX; i++) {
for (int j = 0; j < maxY; j++) {
for (int k = 0; k < maxZ; k++) {
updateX = i;
updateY = j;
updateZ = k;
try {
if (chunks[i][0][k] != null) {
if (Math.abs(chunks[i][0][k].posX - pX) <= renderDistance - 1 && Math.abs(chunks[i][0][k].posZ - pZ) <= renderDistance - 1) {
chunks[i][0][k].loadChunk();
chunks[i][0][k].optimize();
try {
if (chunks[i][0][k].posX == pX && chunks[i][0][k].posZ == pZ || chunks[i][0][k].posX == pX && chunks[i][0][k].posZ == pZ + 1 || chunks[i][0][k].posX == pX && chunks[i][0][k].posZ == pZ - 1 || chunks[i][0][k].posX == pX + 1 && chunks[i][0][k].posZ == pZ || chunks[i][0][k].posX == pX - 1 && chunks[i][0][k].posZ == pZ) {
chunks[i][0][k].loadPhysics();
} else {
chunks[i][0][k].unloadPhysics();
}
} catch (Exception e2) {
}
} else {
chunks[i][0][k].unloadChunk();
}
} else {
}
} catch (Exception e) {
}
}
}
}
}
//replaces the Cell.setId(id), and replaces making all the cell air when chunk is created
public void setCell(int i, int j, int k, Id id) {
//System.out.println("Cell being placed in world coords: " + i + ", " + j + ", " + k);
int plusX = i % chunkSideDim, plusY = j % chunkSideDim, plusZ = k % chunkSideDim;
int chunkX = (i - plusX) / chunkSideDim, chunkY = (j - plusY) / chunkSideDim, chunkZ = (k - plusZ) / chunkSideDim;
try {
if (WorldProvider.chunks[chunkX][chunkY][chunkZ] != null) {
WorldProvider.chunks[chunkX][chunkY][chunkZ].setCell(plusX, plusY, plusZ, id);
} else {
WorldProvider.chunks[chunkX][chunkY][chunkZ] = new Chunk(chunkX, chunkY, chunkZ);
WorldProvider.chunks[chunkX][chunkY][chunkZ].genBase();
WorldProvider.chunks[chunkX][chunkY][chunkZ].setCell(plusX, plusY, plusZ, id);
}
if (!firstCellPlaced) {
PlayerControlState.respawnPoint = new Vector3f(8, 8, 8);
PlayerControlState.playerControl.warp(new Vector3f(8, 8, 8));
firstCellPlaced = true;
}
} catch (ArrayIndexOutOfBoundsException e2) {
//System.out.println("Chunk coords at: " + chunkX + ", " + chunkY + ", " + chunkZ + " is out of the world");
}
}
public Cell getCell(int i, int j, int k) {
//System.out.println("Cell being placed in world coords: " + i + ", " + j + ", " + k);
int plusX = i % chunkSideDim, plusY = j % chunkSideDim, plusZ = k % chunkSideDim;
int chunkX = (i - plusX) / chunkSideDim, chunkY = (j - plusY) / chunkSideDim, chunkZ = (k - plusZ) / chunkSideDim;
try {
if (WorldProvider.chunks[chunkX][chunkY][chunkZ] != null) {
try {
return WorldProvider.chunks[chunkX][chunkY][chunkZ].getCell(plusX, plusY, plusZ);
} catch (Exception e) {
return null;
}
}
} catch (Exception e1) {
return null;
}
return null;
}
//returns the chunk is the specified coords
public Chunk getChunk(int i, int j, int k) {
int plusX = i % chunkSideDim, plusY = j % chunkSideDim, plusZ = k % chunkSideDim;
int chunkX = (i - plusX) / chunkSideDim, chunkY = (j - plusY) / chunkSideDim, chunkZ = (k - plusZ) / chunkSideDim;
return chunks[chunkX][chunkY][chunkZ];
}
public void createHill(int x, int y, int initX, int initY, int height) {
drawEllipse(x, y, initX, initY, 1);
int difference = 0;
int lastDifference = 0;
for (int i = 0; i <= height; i++) {
while (difference <= lastDifference) {
difference = i + rand.nextInt(i + 1) + 1;
}
if (initX - difference > 0 && initY - difference > 0) {
drawEllipse(x, y, initX - difference, initY - difference, i);
}
lastDifference = difference;
}
}
public void drawEllipse(int originX, int originY, int height, int width, int yHeight) {
int hh = height * height;
int ww = width * width;
int hhww = hh * ww;
int x0 = width;
int dx = 0;
// do the horizontal diameter
for (int x = -width; x <= width; x++) {
setCell(originX + x, yHeight, originY, Id.GRASS);
}
// now do both halves at the same time, away from the diameter
for (int y = 1; y <= height; y++) {
int x1 = x0 - (dx - 1); // try slopes of dx - 1 or more
for (; x1 > 0; x1--) {
if (x1 * x1 * hh + y * y * ww <= hhww) {
break;
}
}
dx = x0 - x1; // current approximation of the slope
x0 = x1;
for (int x = -x0; x <= x0; x++) {
setCell(originX + x, yHeight, originY - y, Id.GRASS);
setCell(originX + x, yHeight, originY + y, Id.GRASS);
}
}
}
public void drawCircle(int centerX, int centerY, int radius) {
for (int y = -radius; y <= radius; y++) {
for (int x = -radius; x <= radius; x++) {
if (x * x + y * y <= radius * radius) {
setCell(centerX + x, 0, centerY + y, Id.GRASS);
}
}
}
for (int y = -radius; y <= radius; y++) {
for (int x = -radius; x <= radius; x++) {
if (x * x + y * y <= radius * radius) {
setCell(centerX + x, 0, centerY + y, Id.GRASS);
}
}
}
}
Callable r = new Callable<Object>() {
@Override
public Object call() throws Exception {
System.out.println("Chunk Update Runnable started!");
while (!executor.isShutdown()) {
for(int i = 0; i <= pX +renderDistance; i++){
for(int k = 0; k <= pZ + renderDistance; k++){
if(chunks[i][0][k] == null){
chunks[i][0][k] = new Chunk(i, 0, k);
chunks[i][0][k].genBase();
chunks[i][0][k].update();
}
}
}
}
return null;
}
};
}
The chunk.loadChunk of chunk.unloadChunk method doesn’t affect the game, it just attach of detach the chunk from the terrainNode, attached to the rootNode. Only the update method does.
Thanks,
EmaMaker