Как я мог бы вычислить соседние плитки для создания ребер и углов для массива 2D-карты плиток?

#javascript #procedural-generation

#javascript #процедурная генерация

Вопрос:

У меня есть простой метод, который генерирует 2D-карты островов из симплексного шума, однако он оставляет одну большую проблему. Это работает хорошо, но оставляет острые углы.

Острые углы на 2D-карте плиток

Что я хотел бы сделать, так это взять это и вычислить соседей, чтобы добавить правильные плитки для ребер и углов, но я не уверен, как это добавить. Каков наилучший способ вычислить это?

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

Ответ №1:

Определите, где находятся эти желтые (песочные?) Плитки с краями, у вас есть 4 случая для внешних углов и 4 для внутренних углов.

  -->   -----------   <---  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)
  

Чтобы определить, в каком случае вы смотрите на соседей по ячейке (это вода? в каком направлении …).
Для этих случаев либо используйте специальные плитки make 8 (см. Для этого Классические наборы плиток), либо вы можете поиграть с эффектами прозрачности в альфа-канале.
Как вы можете видеть на примере набора листов, может быть больше случаев: только одна ячейка с водой, только две ячейки с водой … вы можете исключить их из сгенерированной карты, чтобы избежать их.

Удачи.

Ответ №2:

Лучшим объяснением того, как это можно сделать (насколько я смог найти), было бы улучшение генерации процедурных карт — плиток.

Я использовал статью в качестве отправной точки для улучшения генерации карт для игры, которую я создаю (это все еще очень далеко от предполагаемых результатов, но может быть принято за отправную точку):

Это текущий код генерации карты, хотя сейчас он довольно глючный:

 '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 amp;amp; col < current.length - 1) {
                if (val !== TileEdge.A amp;amp; val !== TileEdge.B) {
                    return val;
                }

                const [patTop, patCurrent, patBottom] = pattern;

                if (patternMatches(patBottom, bottom, col) amp;amp;
                   patternMatches(patCurrent, current, col) amp;amp;
                   patternMatches(patTop, top, col)) {              
                    return resu<
                }
            }

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