Terrain Generation Optimization

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();
};

Hi @RumaiIndustries,

Are you generating your terrain per frame? I can you are calling generateTerrain in your postUpdate method.

Try profiling that method using either the browser dev tools (Performance tab) or by adding performance.now() logs. I imagine unless your method is really fast (<5-10ms) then it’s expected to lag in lower end devices with slower CPUs.

Some rough optimizations strategies:

  • Profile your method step by step and check where it’s spending the most time. Optimize and micro-optimize your code.
  • Consider running your method in a lower interval, once every few frames or even better only when there changes to either the player cell or to the actual terrain.

Hope that helps!

Hi @Leonidas ,
Thanks for the response! It seems that the problem is in the amount of Boxes being created and destroyed at once. As you mentioned, generating them every couple of frames seems to work a bit better, but It freezes the game now and then. I have decided to replicate a Minecraft chunk system, however,
I believe that only showing exposed sides of the box might help with performance.

Let’s say that a face is exposed to “Air” (This means that there is no other face less than one unit away), it should be visible. If there is a face less than one unit away, it should not be visible. Could you help me with a way of implementing that onto maybe a plane, or a single side mesh?

In this picture, 2 is air, 1 is a face, and the arrows point to all the other air exposed directions.

image (7)

Thank you!

If you are adding/removing cubes you should consider adding a pool of cubes, so instead of creating/destroying entities, you should be claiming me and returning them in the pool.

Still for a minecraft game you should consider what you said, creating custom meshes using the Mesh API. An example to get started on mesh generation can be found here:

https://playcanvas.github.io/#/graphics/mesh-generation

Hello!

I’ll attempt at using pooling. I have never used the system, so this will prove interesting.
As for the mesh generation, thanks for the resource! I’ll come back soon!

1 Like

Hello @Leonidas!

I used the pooling system, and changed the generation system to use chunks. The game now allows for the generation of hundreds of blocks with minimal lag. I also added chunk generation settings, so that the amount of chunks can be changed to decrease lag and freezing. Thanks for helping!

1 Like