2

I have a simple method that generates 2D island maps from SimpleX Noise, however it's leaving one larger issue. It works well, but it leaves sharp corners.

Sharp corners on 2D Tile Map

What I would like to do is take this and calculate neighbors to add the correct tiles for edges and corners, but I am not sure how to add this in. What would be the best way to calculate this?

generateMap()
{
    let outputMap = [];

    for(let y = 0; y < this.tileCount; y++)
    {
        outputMap[y] = [];
        for(let x = 0; x < this.tileCount; x++)
        {
            let nx = x / this.tileCount - 0.5, ny = y / this.tileCount - 0.5;
            let e = 1 + +Math.abs(this.heightMapGen.noise2D(1 * nx, 1 * ny));
            e += 0.5 + +Math.abs(this.heightMapGen.noise2D(2 * nx, 2 * ny));  
            e += 0.25 + +Math.abs(this.heightMapGen.noise2D(4 * nx, 4 * ny));  


            let output = (Math.pow(e, 0.21) % 1).toFixed(2).split(".");
            outputMap[y][x] = parseFloat(output[1]);

            if (outputMap[y][x] <= 25)
            {
                outputMap[y][x] = 0; // Water //
            } else {
                // Terrain //
                switch(outputMap[y][x])
                {
                    case 28:
                    case 29:
                        outputMap[y][x] = 2;
                    break;                        
                    case 27:
                        outputMap[y][x] = 1;
                    break;
                    case 26:
                        outputMap[y][x] = 4;
                    break;
                    default:
                        outputMap[y][x] = 3;
                }                
            }  
        }
    }

    return outputMap;
}
Merlin
  • 929
  • 12
  • 33

2 Answers2

1

Determine where those yellow (sand ?) edge tiles are, you have 4 cases for external corners and 4 for internal corners.

+-->  +-----------+  <---+ corner tile (one case)
      |         |_|  
      |    land   |  sea here
      |    here   |
      |           |
      |           |
+-->  +-----------+  <----+

4 external corners (sea shore, beach)


      +-----------+
      | land here |
+-------> +---+ <-------+
      |   |wat|   |
      |   |er |   |
+-------> +---+ <-------+
      |           |
      +-----------+

4 potential internal corners (lake shore or pond edge)

To determine in which case you are look at the cell neighbors (is it water ? in which direction ...). Either use make 8 specific tiles for those cases (see classic tilesets for that), or you can play with transparency effects on the alpha channel. As you can see on the tileset example, there could be more cases: just one cell of water, just two cells with water ... you may eliminate those from your generated map to avoid them.

Good luck.

Christophe Roussy
  • 16,299
  • 4
  • 85
  • 85
0

The best explanation on how this could be done (as far as I could find) would be Enhancing Procedural Maps - Tiles Generation.

I have used the article as a start point for improvements on the map generation for the game I'm making (it's still very far from the intended results, but could be taken as a starting point):

This is the current map generation code, though it is pretty glitchy right now:

'use strict';

import {range, sample} from 'lodash';
import SimplexNoise from 'simplex-noise';

enum TileEdge {
    A, B, L, R,
    BOTTOM, TOP,
    ITL, ITR, IBL, IBR,
    OTL, OTR, OBL, OBR
};

const tileEdgeNames = (() => {
    const names = [];
    for (let item in TileEdge) {
        if (isNaN(Number(item))) {
            names.push(item.toLowerCase());
        }
    }
    return names;
})();
console.log(tileEdgeNames);

export const desertTileIndexes = {
    a: [16, 46],
    b: [50, 51, 60, 61,62, 63],
    l: [15],
    r: [17],
    top: [6],
    bottom: [26],

    itl: [5],
    itr: [7],
    ibl: [25],
    ibr: [27],

    otl: [8],
    otr: [9],
    obl: [18],
    obr: [19]
};

export const cloudTileIndexes = {
    a: [13, 20, 21],
    b: [-1],
    l: [12],
    r: [14],
    top: [3],
    bottom: [23],

    itl: [2],
    itr: [4],
    ibl: [22],
    ibr: [24],

    otl: [0],
    otr: [1],
    obl: [10],
    obr: [11]
};

export const rockTileIndexes = {
    a: [76, 70, 71, 72, 73, 74],
    b: [-1],
    l: [75],
    r: [77],
    top: [66],
    bottom: [86],

    itl: [68],
    itr: [69],
    ibl: [78],
    ibr: [79],

    otl: [65],
    otr: [67],
    obl: [85],
    obr: [87]
};

const generateMainPlanes = (position, {tileCount = 10, threshold = 0, noiseFunction} = {}): TileEdge[] => {
    return range(tileCount).map((j) => noiseFunction(position, j) > threshold ? TileEdge.B : TileEdge.A;
};


const generateTileIndexes = ([bottom, current, top], {tileTypeIndexes} = {}) => {
    return current.map((idx, col) => sample(tileTypeIndexes[tileEdgeNames[idx]]));
};

const createStripGenerator = ({tileCount = 10, threshold = 0} = {}) => {    
    const simplex = new SimplexNoise();
    const noiseFunction = (x, y) => simplex.noise2D(x, y);

    const options = {tileCount, threshold, noiseFunction};

    let position = 0;

    return () => {
        return generateMainPlanes(position++, options);
    }
}

const holePatchingAutomata = [
    {
        pattern: [
            '???',
            '*.*',
            '???',
        ],
        result: TileEdge.A
    },
    {
        pattern: [
            '?*?',
            '?.?',
            '?*?',
        ],
        result: TileEdge.A
    },
    {
        pattern: [
            '??*',
            '?.?',
            '*??',
        ],
        result: TileEdge.A
    },
    {
        pattern: [
            '*??',
            '?.?',
            '??*',
        ],
        result: TileEdge.A
    },
];

const cornerAutomata = [

    {
        pattern: [
            '?.?',
            '?.*',
            '?.?',
        ],
        result: TileEdge.L
    },
    {
        pattern: [
            '?.?',
            '*.?',
            '?.?',
        ],
        result: TileEdge.R
    },
    {
        pattern: [
            '???',
            '...',
            '?*?',
        ],
        result: TileEdge.TOP
    },
    {
        pattern: [
            '?*?',
            '...',
            '???',
        ],
        result: TileEdge.BOTTOM
    },


    // Inner corner
    {
        pattern: [
            '?*?',
            '*.?',
            '???',
        ],
        result: TileEdge.ITL
    },
    {
        pattern: [
            '?*?',
            '?.*',
            '???',
        ],
        result: TileEdge.ITR
    },
    {
        pattern: [
            '???',
            '*.?',
            '?*?',
        ],
        result: TileEdge.IBL
    },
    {
        pattern: [
            '???',
            '?.*',
            '?*?',
        ],
        result: TileEdge.IBR
    },

    // Outer corners
    {
        pattern: [
            '???',
            '?..',
            '?.*',
        ],
        result: TileEdge.OTL
    },
    {
        pattern: [
            '???',
            '..?',
            '*.?',
        ],
        result: TileEdge.OTR
    },
    {
        pattern: [
            '?.*',
            '?..',
            '???',
        ],
        result: TileEdge.OBL
    },
    {
        pattern: [
            '*.?',
            '..?',
            '???',
        ],
        result: TileEdge.OBR
    },

];

const patternTokenHandlers = {
    '?': () => true,
    '.': x => x === TileEdge.B,
    '*': x => x === TileEdge.A
};

const patternMatches = (expected, actual, offset) => {
    for (let i = 0, j = offset - 1; i < expected.length; i++, j++) {
        if (!patternTokenHandlers[expected[i]](actual[j])) {
            return false;
        }           
    }

    return true;
}

const applyAutomata = (automata, [bottom, current, top]) => {
    const currentModified = current.map((idx, col) => {
        return automata.reduce((val, {pattern, result}) => {
            if (col > 0 && col < current.length - 1) {
                if (val !== TileEdge.A && val !== TileEdge.B) {
                    return val;
                }

                const [patTop, patCurrent, patBottom] = pattern;

                if (patternMatches(patBottom, bottom, col) &&
                   patternMatches(patCurrent, current, col) &&
                   patternMatches(patTop, top, col)) {              
                    return result;
                }
            }

            return val;
        }, idx);
    });

    return [bottom, currentModified, top];
}

export const mapGenerator = ({tileCount = 10, threshold = 0, tileTypeIndexes} = {}) => {
    const generateStrip = createStripGenerator({tileCount, threshold});

    let bottom;
    let current = generateStrip();
    let top = generateStrip();

    return () => {
        const currentStrips = [bottom, current, top] = [current, top, generateStrip()];     
        const withHolesPatched = applyAutomata(holePatchingAutomata, currentStrips);
        const withCorners = applyAutomata(cornerAutomata, withHolesPatched);
        return generateTileIndexes(withCorners, {tileTypeIndexes});
    }
};
Haroldo_OK
  • 6,612
  • 3
  • 43
  • 80