Getting an Adjacent Block Path

Home --> Programming Projects --> Fractal Block Engine --> Procedural World Generation --> Getting an Adjacent Block Path

The problem

Given the path of a chunk, we sometimes want to compute the paths of adjacent chunks.

Although this is used for procedural world generation, it is used in other parts of the engine as well.

Block Path Class

Here are the key parts of a class to store the position of a block within a chunk. Remember that a chunk is 16x16x16 blocks. This is called a Local Block Position. See here for more on these data types.
class LocalBlockPos {
public:
    signed char x;
    signed char y;
    signed char z;
    
    LocalBlockPos();
    
    LocalBlockPos(
        signed char x,
        signed char y,
        signed char z);

    int Hash() const {
        return z + (16 * y) + (256 * x);
    }
    
    void SetFromHash(int hash) {
        z = hash % 16;
        code /= 16;
        y = hash % 16;
        code /= 16;
        x = hash % 16;
    }
};
Note how we can encode a LocalBlockPosition as a single (4 byte) integer. We could actually store it in a 2 byte short, but the current method works well enough.

Here are the key parts of the BlockPath class:
class BlockPath {
public:
    vector<LocalBlockPos> path;

    BlockPath();

    void GetCoords(
        vector<int> & x_coords,
        vector<int> & y_coords,
        vector<int> & z_coords) const;

    void SetCoords(
        const vector<int> & x_coords,
        const vector<int> & y_coords,
        const vector<int> & z_coords);

    string ToString() const;
};
The ToString function on a BlockPath might output the following:
59a_f85_776_8c9

Note that instead of storing a path using the BlockPath class, we could store it is a vector of integers, where each integer is the hash of a LocalBlockPos. This is what the BlockHashPath class does (which the engine uses).

Long Addition

Long addition is the basic engine behind finding an adjacent block path (like we learn in grade school, along with long division).

Given a block path, we consider the x components separate from the y components separate from the z components. We take the x components and put them into a vector. We can think of this as representing a very large integer.

Here is the long addition algorithm. We only care about base = 16.
bool LongAddition(
    int base,
    const svector<int> & input,
    int change,
    svector<int> & changes,
    svector<int> & result)
{
    //Resizing the changes vector.
    changes.resize( input.size() );

    result = input; //Copying.
    //
    result[ result.size() - 1 ] += change;
    for(int i = result.size() - 1; i >= 0; i--) {
        changes[i] = result[i] - input[i];
        while( result[i] >= base ) {
            if( i == 0 ) return false;
            result[i] -= base;
            result[i-1] += 1;
        }
        while( ret[i] < 0 ) {
            if( i == 0 ) return false;
            result[i] += base
            result[i-1] -= 1;
        }
    }

    return true;
}
The function returns false if the resulting vector of coordinates would no longer be in the world.

The change value is added to the last element of the vector of coordinates.

This may cause "carrying over". The vector "changes" is returned and that shows where there was a carry over. This is needed by some parts of the engine. For example, when the player moves to an adjacent chunk, we need to know what levels need to be "shifted". A level needs to be shifted iff the chunk on that level that contains the player changes to a different chunk. However in this page we can ignore these carrying over values given by "changes". The reader can choose to make their own version of LongAddition that does not have a "changes" vector.

If the function returns true, the "result" vector is the new (shifted) vector of coordinates.

For example, suppose the "input" vector is
7 15 15
and "base" is set to 16 and "change" is set to 1. Then the LongAddition function will first add a 1 to the last coordinate, which will carry over, which will result in a second carry over. The "result" vector will be
8 0 0.

Code for Getting an Adjacent Path

We now have all the pieces for the code to get an adjacent path:
bool ComputeNearbyPath(
    const BlockPath & old_path,
    int dx, int dy, int dz,
    BlockPath & new_path)
{
    int base = 16;

    vector<int> old_x_coords;
    vector<int> old_y_coords;
    vector<int> old_z_coords;
    old_path.GetCoords(
        old_x_coords, old_y_coords, old_z_coords);
    
    //Will not use.
    vector<int> changes_x;
    vector<int> changes_y;
    vector<int> changes_z;

    vector<int> new_x_coords;
    vector<int> new_y_coords;
    vector<int> new_z_coords;

    if( !LongAddition(
        base, old_x_coords, dx, changes_x, new_x_coords) )
    {
        return false;
    }

    if( !LongAddition(
        base, old_y_coords, dy, changes_y, new_y_coords) )
    {
        return false;
    }

    if( !LongAddition(
        base, old_z_coords, dz, changes_z, new_z_coords) )
    {
        return false;
    }

    new_path.SetCoords(new_x_coords, new_y_coords, new_z_coords);
    return true;
}
This function returns false if the new path is not in the world. In this case, "new_path" is not valid.