Hello!
I have been working on a terrain generation system like the one seen in Minecraft.
It works exactly as intended, however, it causes extreme amounts of
lag on low end devices when new terrain loads in. I was hoping someone could help me find a work around. Thanks for reading!
Code:
var TerrainGenerator = pc.createScript('TerrainGenerator');
// Source block prefab for the ground
TerrainGenerator.attributes.add('sourceBlockPrefab', { type: 'entity' });
// Source block prefab for the tree trunk
TerrainGenerator.attributes.add('sourceBlockPrefab1', { type: 'entity' });
// Source block prefab for the tree top
TerrainGenerator.attributes.add('sourceBlockPrefab2', { type: 'entity' });
// Source block prefab for the rocks
TerrainGenerator.attributes.add('sourceBlockPrefab3', { type: 'entity' });
// Source block prefab for the bedrock
TerrainGenerator.attributes.add('sourceBlockPrefabBedrock', { type: 'entity' });
// Dimensions of the terrain
TerrainGenerator.attributes.add('terrainWidth', { type: 'number', default: 100 });
TerrainGenerator.attributes.add('terrainDepth', { type: 'number', default: 50 });
TerrainGenerator.attributes.add('terrainHeight', { type: 'number', default: 10 });
// Character entity reference
TerrainGenerator.attributes.add('characterEntity', { type: 'entity' });
// Block size and spacing
TerrainGenerator.attributes.add('blockSize', { type: 'number', default: 1.48 });
// Button attributes for controlling distances
TerrainGenerator.attributes.add('distanceButton5', { type: 'entity', title: 'Distance 5', description: 'Button for setting generation and de-render distance to 5' });
TerrainGenerator.attributes.add('distanceButton15', { type: 'entity', title: 'Distance 15', description: 'Button for setting generation and de-render distance to 15' });
TerrainGenerator.attributes.add('distanceButton25', { type: 'entity', title: 'Distance 25', description: 'Button for setting generation and de-render distance to 25' });
TerrainGenerator.prototype.initialize = function () {
// Create an empty map to track generated blocks
this.generatedBlocks = {};
this.generatedTrees = {};
// Set the generation and de-render distances to 5 by default
this.generationDistance = 5;
this.treeDeRenderDistance = 5;
// Generate initial terrain
this.generateTerrain();
// Subscribe to the postUpdate event
this.app.on('postUpdate', this.postUpdate, this);
// Set up button click events
this.distanceButton5.element.on('click', this.onDistanceButtonClicked.bind(this, 5));
this.distanceButton15.element.on('click', this.onDistanceButtonClicked.bind(this, 15));
this.distanceButton25.element.on('click', this.onDistanceButtonClicked.bind(this, 25));
};
TerrainGenerator.prototype.onDistanceButtonClicked = function (distance) {
// Set the generation and de-render distances to the clicked value
this.generationDistance = distance;
this.treeDeRenderDistance = distance;
};
TerrainGenerator.prototype.generateTerrain = function () {
var offsetX = (this.terrainWidth - 1) * 0.5; // Calculate the offset in x-axis
var offsetZ = (this.terrainDepth - 1) * 0.5; // Calculate the offset in z-axis
// Calculate the grid position of the character within the terrain
var gridX = Math.floor(this.characterEntity.getPosition().x / this.blockSize);
var gridZ = Math.floor(this.characterEntity.getPosition().z / this.blockSize);
// Calculate the start and end positions of the terrain to generate
var startX = gridX - this.generationDistance;
var endX = gridX + this.generationDistance;
var startZ = gridZ - this.generationDistance;
var endZ = gridZ + this.generationDistance;
// Loop through each position in the generation range and create blocks
for (var x = startX; x <= endX; x++) {
if (!this.generatedBlocks[x]) {
this.generatedBlocks[x] = {};
}
for (var z = startZ; z <= endZ; z++) {
if (!this.generatedBlocks[x][z]) {
var xPos = x * this.blockSize; // Use x directly as position without offset
var zPos = z * this.blockSize; // Use z directly as position without offset
// Generate terrain height (simple hill algorithm)
var noise = Math.sin(x / 10) + Math.sin(z / 10);
var yPos = Math.floor(noise * this.terrainHeight) + 15; // Add 15 to the y-position to shift terrain up
// Randomly select a block prefab for the first layer
var blockPrefab;
if (Math.random() < 0.99) {
blockPrefab = this.sourceBlockPrefab.clone(); // Grass block prefab
} else {
blockPrefab = this.sourceBlockPrefab3.clone(); // Rock block prefab
}
blockPrefab.enabled = true;
blockPrefab.setPosition(xPos, yPos - this.blockSize, zPos); // Adjust the y-position by subtracting blockSize
var collision = blockPrefab.addComponent('collision', {
type: 'box',
halfExtents: new pc.Vec3(this.blockSize * 0.5, this.blockSize * 0.5, this.blockSize * 0.5)
});
this.app.root.addChild(blockPrefab);
this.generatedBlocks[x][z] = {
layer1: blockPrefab
};
// Randomly generate a tree on top of the block, avoiding other trees
if (Math.random() < 0.003) { // Adjust the probability as desired
var treePositionValid = this.isTreePositionValid(x, z);
if (treePositionValid) {
var tree = this.generateTree();
tree.setPosition(xPos, yPos - 4, zPos); // Adjust the tree position 4 units below the block
this.app.root.addChild(tree);
this.generatedTrees[x + ':' + z] = tree;
}
}
// Randomly select a block prefab for the second layer
var blockPrefab2;
if (Math.random() < 0.99) {
blockPrefab2 = this.sourceBlockPrefabBedrock.clone(); // Grass block prefab
} else {
blockPrefab2 = this.sourceBlockPrefabBedrock.clone(); // Rock block prefab
}
blockPrefab2.enabled = true;
blockPrefab2.setPosition(xPos, yPos - (2 * this.blockSize), zPos); // Adjust the y-position by subtracting 2 * blockSize
var collision2 = blockPrefab2.addComponent('collision', {
type: 'box',
halfExtents: new pc.Vec3(this.blockSize * 0.5, this.blockSize * 0.5, this.blockSize * 0.5)
});
this.app.root.addChild(blockPrefab2);
this.generatedBlocks[x][z].layer2 = blockPrefab2; // Assign the second layer block to the generatedBlocks object
}
}
}
};
TerrainGenerator.prototype.isTreePositionValid = function (x, z) {
var minDistance = 10;
for (var key in this.generatedTrees) {
var treeGridPos = key.split(':');
var treeGridX = parseInt(treeGridPos[0], 10);
var treeGridZ = parseInt(treeGridPos[1], 10);
var distance = Math.sqrt(Math.pow(x - treeGridX, 2) + Math.pow(z - treeGridZ, 2));
if (distance < minDistance) {
return false;
}
}
return true;
};
TerrainGenerator.prototype.generateTree = function () {
// Create a tree entity
var tree = new pc.Entity();
// Calculate the tree position offset
var offsetY = this.blockSize * 3; // Adjust the tree height as desired
// Create the tree trunk blocks
for (var i = 0; i < 4; i++) {
var trunkBlock = this.sourceBlockPrefab1.clone();
trunkBlock.enabled = true;
trunkBlock.setPosition(0, (i * this.blockSize) + offsetY, 0); // Adjust the offset by multiplying by the block size
tree.addChild(trunkBlock);
}
// Create the tree top blocks
var topBlockSize = 5;
var halfTopBlockSize = Math.floor(topBlockSize / 2);
var topBlockOffsetY = (4 * this.blockSize) + offsetY; // Adjust the offset by multiplying by the block size
// Duplicate the top layer twice
for (var layer = 0; layer < 2; layer++) {
var layerOffsetY = topBlockOffsetY + (layer * this.blockSize); // Adjust the offset for each layer
for (var x = -halfTopBlockSize; x <= halfTopBlockSize; x++) {
for (var z = -halfTopBlockSize; z <= halfTopBlockSize; z++) {
var topBlock = this.sourceBlockPrefab2.clone();
topBlock.enabled = true;
topBlock.setPosition(x * this.blockSize, layerOffsetY, z * this.blockSize);
tree.addChild(topBlock);
}
}
}
// Create the third layer (3x3) on top of the tree
var thirdLayerOffsetY = topBlockOffsetY + (2 * this.blockSize); // Adjust the offset for the third layer
for (var x = -1; x <= 1; x++) {
for (var z = -1; z <= 1; z++) {
var topBlock = this.sourceBlockPrefab2.clone();
topBlock.enabled = true;
topBlock.setPosition(x * this.blockSize, thirdLayerOffsetY, z * this.blockSize);
tree.addChild(topBlock);
}
}
// Create the fourth layer (+ shape)
var fourthLayerOffsetY = topBlockOffsetY + (3 * this.blockSize); // Adjust the offset for the fourth layer
var plusBlock1 = this.sourceBlockPrefab2.clone();
plusBlock1.enabled = true;
plusBlock1.setPosition(0, fourthLayerOffsetY, -this.blockSize);
tree.addChild(plusBlock1);
var plusBlock2 = this.sourceBlockPrefab2.clone();
plusBlock2.enabled = true;
plusBlock2.setPosition(0, fourthLayerOffsetY, 0);
tree.addChild(plusBlock2);
var plusBlock3 = this.sourceBlockPrefab2.clone();
plusBlock3.enabled = true;
plusBlock3.setPosition(0, fourthLayerOffsetY, this.blockSize);
tree.addChild(plusBlock3);
var plusBlock4 = this.sourceBlockPrefab2.clone();
plusBlock4.enabled = true;
plusBlock4.setPosition(-this.blockSize, fourthLayerOffsetY, 0);
tree.addChild(plusBlock4);
var plusBlock5 = this.sourceBlockPrefab2.clone();
plusBlock5.enabled = true;
plusBlock5.setPosition(this.blockSize, fourthLayerOffsetY, 0);
tree.addChild(plusBlock5);
return tree;
};
TerrainGenerator.prototype.destroyTerrain = function () {
var offsetX = (this.terrainWidth - 1) * 0.5; // Calculate the offset in x-axis
var offsetZ = (this.terrainDepth - 1) * 0.5; // Calculate the offset in z-axis
// Calculate the grid position of the character within the terrain
var gridX = Math.floor(this.characterEntity.getPosition().x / this.blockSize);
var gridZ = Math.floor(this.characterEntity.getPosition().z / this.blockSize);
// Calculate the start and end positions of the terrain to destroy
var startX = gridX - (this.generationDistance + 1);
var endX = gridX + (this.generationDistance + 1);
var startZ = gridZ - (this.generationDistance + 1);
var endZ = gridZ + (this.generationDistance + 1);
// Loop through each generated block and destroy if outside the generation range
for (var x in this.generatedBlocks) {
for (var z in this.generatedBlocks[x]) {
if (x < startX || x > endX || z < startZ || z > endZ) {
var block = this.generatedBlocks[x][z];
block.layer1.destroy(); // Destroy the first layer block
// Check if the second layer block exists and destroy it
if (block.layer2) {
block.layer2.destroy();
delete block.layer2;
}
// Check if the third layer block exists and destroy it
if (block.layer3) {
block.layer3.destroy();
delete block.layer3;
}
delete this.generatedBlocks[x][z];
}
}
}
// Loop through each generated tree and de-render if outside the de-render distance
for (var key in this.generatedTrees) {
var tree = this.generatedTrees[key];
var treeGridPos = key.split(':');
var treeGridX = parseInt(treeGridPos[0], 10);
var treeGridZ = parseInt(treeGridPos[1], 10);
if (treeGridX < startX || treeGridX > endX || treeGridZ < startZ || treeGridZ > endZ) {
tree.enabled = false;
} else {
tree.enabled = true;
}
}
};
TerrainGenerator.prototype.postUpdate = function () {
// Generate new terrain
this.generateTerrain();
// Destroy old terrain
this.destroyTerrain();
};