4
\$\begingroup\$

Following the notice that block events will be removed in a future update, I have been implementing custom block mechanics (such as crop growth and groundcover spreading) using server-side scripts instead. This particular script accompanies a custom block called 'rhizome' which randomly spreads to adjacent blocks, and decays if covered by a solid block, in the same way as vanilla grass/mycelium blocks do.

It works exactly as intended, and doesn't seem to be causing perceptible lag, but occasionally displays warnings of the type
[Scripting][warning]-[Watchdog] 180.289 ms script spike detected in behavior pack 'PackName'
which leads me to believe it may be unreasonably resource intensive. (For comparison, I have implemented multiple other custom features using scripts, including some that run many sequential setblock and fill commands, and have never gotten this kind of warning before.)

The problem seems to be the nested for loops to check for all blocks of a certain type within 16 blocks of the player: reducing the radius from 20 down to the current 16 blocks decreased the number of milliseconds in the warning from ~340 to ~180, indicating that this is indeed the bottleneck.

It doesn't seem like there is a built-in API method for getting all blocks of a certain type within an area, like there is for entities (with dimension.getEntities()). No such method is listed in the dimension documentation. So, I'm not sure that there's an alternative to using nested loops like this, but would like to know if there are any performance improvements possible without decreasing the radius even further.

While 180 milliseconds doesn't seem like much of an issue, I have only tested my addon in singleplayer, and anticipate it could be a much larger problem on a multiplayer server where allPlayers.length > 1. I would like the addon to be compatible with multiplayer as well as singleplayer worlds.

import { world, system } from "@minecraft/server"

const worldHeightLimit = 320;
const worldDepthLimit = -64;

/* Within 16 blocks of a player, check for rhizome spreading to adjacent end stone blocks, or decaying if there is a solid block directly on top of it */
function processRhizome(player) {
    var centerX = player.location.x;
    var centerY = player.location.y;
    var centerZ = player.location.z;
    var dim = player.dimension;
    
    var rB = []; // Will store all rhizome blocks
    var block;
    
    for (var y = -16; y <= 16; y++) {
        // If layer is below bottom bedrock level, skip to next layer
        if ((centerY + y) < worldDepthLimit) {
            continue;
        } else if ((centerY + y) > worldHeightLimit) { // If layer is above height limit, exit loop
            break;
        } else {
            for (var x = -16; x <= 16; x++) {
                for (var z = -16; z <= 16; z++) {
                    block = dim.getBlock({x: centerX + x, y: centerY + y, z: centerZ + z});
                    if (block.typeId === "end:rhizome") {
                        rB.push(block);
                    }
                }
            }
        }
    }
    /* If a rhizome block has adjacent end stone block neighbors (which do not have a solid block directly above them), generate a random number to test whether the rhizome should spread.
       If yes, then choose an end stone block from among the neighboring blocks at random and convert it to another rhizome block.
       
       If the rhizome block has a solid block directly above it, generate a different random number to test whether the rhizome block should decay into end stone. */
    var b;
    for (var i = 0; i < rB.length; i++) {
        b = rB[i];
        // Decay chance = 25% per random tick
        if (!b.above(1).isAir && !b.above(1).isLiquid && (Math.random() < 0.25)) {
            (async () => {
                await dim.runCommandAsync("setblock " + b.x + " " + b.y + " " + b.z + " minecraft:end_stone replace");
            })();
        } else if (Math.random() < 0.05) { // Spread chance = 5% per random tick
            var neighbors = [];
            
            for (var y = -1; y <= 1; y++) {
                if ((b.y + y) < worldDepthLimit) {
                    continue;
                } else if ((b.y + y) > worldHeightLimit) {
                    break;
                } else {
                    for (var x = -1; x <= 1; x++) {
                        for (var z = -1; z <= 1; z++) {
                            block = dim.getBlock({x: b.x + x, y: b.y + y, z: b.z + z});
                            // Do not add block to neighbors array if it is directly under a solid block, or if it is directly above the current rhizome block
                            if (block.typeId === "minecraft:end_stone" && (block.above(1).isAir || block.above(1).isLiquid) && ([x, y, z].toString() !== "0,1,0")) {
                                neighbors.push(block);
                            }
                        }
                    }
                }
            }
            if (neighbors.length > 0) {
                var spreadToBlock = neighbors[Math.floor(Math.random()*neighbors.length)];
                (async () => {
                    await dim.runCommandAsync("setblock " + spreadToBlock.x + " " + spreadToBlock.y + " " + spreadToBlock.z + " end:rhizome replace");
                })();
            }
        }
    }
}

function doRandomTickEventsForEachPlayer() {
    const allPlayers = world.getAllPlayers();
    for (var p = 0; p < allPlayers.length; p++) {
        processRhizome(allPlayers[p]);
    }
    var nextInterval = 60 + Math.round(Math.random()*40); // Between 3 and 5 seconds
    system.runTimeout(doRandomTickEventsForEachPlayer, nextInterval);
}
doRandomTickEventsForEachPlayer();
\$\endgroup\$
2
  • 2
    \$\begingroup\$ I have rolled back Rev 2 → 1. Please see What should I do when someone answers my question? - specifically the sections What should I not do? and I improved my code based on the reviews. What next?. \$\endgroup\$ Feb 26 at 7:57
  • 1
    \$\begingroup\$ @SᴀᴍOnᴇᴌᴀ "Do not add an improved version of the code after receiving an answer. Including revised versions of the code makes the question confusing" -- thanks, I wasn't aware of that rule on this specific site. \$\endgroup\$ Feb 26 at 21:37

2 Answers 2

2
\$\begingroup\$

While you could go about optimizing the detection of rhizome blocks using deltas, caching, and all sorts of smart heuristics, there is also a simpler optimization available that utilizes the probabilistic nature of your actions.

Currently, you check every block to test if its rhizome, and then if it is you still ignore its decay 75% of the time and its spreading 95% of the time. What would be better would be to only check 25% of the blocks for rhizome, and decay these (if applicable) 100% of the time (similarly, check 5% of the blocks for rhizome to spread).

This alone is a ~4x speedup, but you could also reduce the lag spikes by smoothing out your random ticks using a similar trick. You could check half as many blocks for decay, twice as often, resulting in smaller lag spikes (or any fraction you want). This of course won't improve your overall efficiency, it will just make the server run smoother.

In reality, you're probably best off merging these improvements with some of the optimizations shared by J_H, so you get the free 4x speedup on top of some heuristics to generally avoid wasting time searching non-rhizome blocks.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Thanks, @DillonDavis! +1 (and I really wish it were possible to accept both answers) :) I'll be starting with your suggestions since they require a lot less drastic of a change to the way my script currently works. \$\endgroup\$ Feb 21 at 22:49
3
+50
\$\begingroup\$

Is it possible to make this Minecraft Bedrock addon script less resource intensive?

Sure. Do less work.

Currently you are triggering runs of this "scan a thousand blocks" routine quite frequently, and you are immediately notified of rhizome blocks as they crop up on your horizon, 16 units away.

If you only scanned every other cycle, CPU usage goes down 50%, and you might get delayed notification that such a block had silently crept up on you and is now just 15 units away. Scan even less frequently, and perhaps first notification is at a distance of 12 units away.

We could do this in a more principled way. Note the time, set a budget of 80 msec of scanning, do a raster row of blocks at a time, and check your stopwatch at the end of each raster. If we blew the budget, note where we are and abandon the scan. Next time a scan is triggered, pick up from where we left off.

player movement

Maybe rhizomes don't move or spawn much, and what's really happening is the player keeps moving near existing rhizomes. Well, that's an easy problem, right?

If player is moving a unit north, we have just a single new raster of blocks toward the north to scan. It's not like re-scanning blocks to the south will show anything new -- we already scanned them a moment ago. So remember player's position from previous scan, and let the delta inform our current scan, so we only look at the leading edge.

clumps

Suppose that any given rhizome block is likely to be adjacent to a neighboring rhizome block. They appear in clumps.

Then most raster scans will come up empty, and if a scan does get a hit, it will likely get several adjacent hits.

We can use this to our advantage. Take the thousand (dx, dy) delta offsets we're scheduled to scan, and shuffle them so we'll probe them in random order. Set an 80 msec budget and do as much of a scan as will fit, checking our stopwatch every dozen probes or so. The time expires, then we're re-triggered, and we pick up where we left off, in the middle of that permutation of offsets. At some point we get a hit. Only then do we start probing adjacent blocks, to quickly explore the newly discovered clump.

We can combine schedules, for example every other probe is a leading edge probe, followed by a random permutation probe. Or focus on leading edge when there's player movement, and on the random permutation during a pause in movement.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.