0
\$\begingroup\$

I'm trying to generate maps (as in terrain maps) using JavaScript only, but currently performance is pretty bad at best.

What my script currently does is:

  1. Generate a square base grid for the map and print it
  2. For each cell, check the adjacent cell's terrain type and select the terrain for the current cell
  3. Print the results

How can I improve performance? (I'm aware that the current result looks nothing like an actual map, but one thing at a time.)

I've hosted my current script here.

(base and terrain properties are CSS classes)

$(document).ready(function () {
    var x = 50;
    var y = 50;
    var worldMap = [];

    worldMap = genesis(x, y, worldMap);
    worldMap = terraform(x, y, worldMap);
});

function genesis(x, y, worldMap) {
    for (i = 0; i < x; i++) {
        worldMap[i] = [];
        for (var j = 0; j < y; j++) {
            worldMap[i][j] = {
                xCoord: i,
                yCoord: j,
                base: "cell",
                terrain: "baren"
            };
            //console.log(worldMap[i][j]);
        }
    }

    var world = $(".world");

    for (i = 0; i < x; i++) {
        var row = document.createElement("div");
        row.className = "row";

        for (j = 0; j < y; j++) {
            var cell = document.createElement("div");
            cell.className = worldMap[i][j].base + " " + worldMap[i][j].terrain;
            cell.setAttribute('data-row', i);
            cell.setAttribute('data-col', j);
            row.appendChild(cell);
        }

        world.append(row);
    }

    return worldMap;
}


function selectTerrain(worldMap, cell) {
    var terrain = ["river", "river", "forest", "forest", "plain", "plain", "plain", "desert", "mountain"];

    var i = cell.xCoord;
    var j = cell.yCoord;

    console.log(i + " " + j);

    for (var a = -1; a < 2; a++) {
        for (var b = -1; b < 2; b++) {
            var m = parseInt(i) + parseInt(a);
            var n = parseInt(j) + parseInt(b);

            if (typeof worldMap[m] != "undefined") {
                if (typeof worldMap[m][n] != "undefined") {
                    var adj = worldMap[m][n];
                    console.log(adj);
                    console.log("Is defined");
                    if (adj.terrain != "baren") {
                        console.log("Is not baren");
                        for (z = 0; z < 4; z++) {
                            terrain.push(adj.terrain);
                        }
                        console.log(terrain);
                    }
                }
            }

        }
    }

    var selectedIndex = Math.floor(Math.random() * (terrain.length - 0)) + 0;
    var selectedTerrain = terrain[selectedIndex];

    return selectedTerrain;
}

function terraform(x, y, worldMap) {
    for (var i = (x-1); i-- ;) {
        for (var j = (y-1); j--; ) {
            var mappingCell = worldMap[i][j];
            console.log(mappingCell);
            var selectedTerrain = selectTerrain(worldMap, mappingCell);

            worldMap[i][j].terrain = selectedTerrain;

            console.log(mappingCell.terrain);

            var cell = $("[data-row=" + i + "][data-col=" + j + "]");
            cell[0].className = mappingCell.base + " " + mappingCell.terrain;
        }
    }

    return worldMap;
}
\$\endgroup\$
2
  • \$\begingroup\$ Welcome to Code Review. The current question title, which states your concerns about the code, applies to too many questions on this site to be useful. The site standard is for the title to simply state the task accomplished by the code. Please see How to Ask for examples, and revise the title accordingly. \$\endgroup\$
    – Zeta
    Mar 29 '18 at 13:52
  • \$\begingroup\$ @Zeta Title corrected. Thanks for the heads-up! \$\endgroup\$ Mar 29 '18 at 14:01
1
\$\begingroup\$

That map looks super cool! The performance isn't THAT bad, considering you're generating 2500 DIV elements :)

What you could do is generate one Canvas instead of so many DIVS. The GPU can render a canvas super-fast. That will also be helpful if you start adding more elements to your game.

There is no reason to use jQuery, so you can get rid of that too :)

\$\endgroup\$
0
\$\begingroup\$

Canvas && tile maps

First drop the jQuery you dont need it and it will just slow everything down.

Canvas

Displaying the map is much better done via a canvas. Each canvas pixel does not need an interface and can be a million (no exaggeration) times faster than a DOM element and use a tiny fraction of the memory.

Though you lose the functionality of a DOM element it is very easy to implement the basic needs using the canvas.

The canvas is a very powerful rendering surface. The 2D context is fully hardware accelerated and can do full screen 2D games at 60fps on average machines and on good machines it just awesome. It can also do 3D via webGL or as a hybrid 2D context and webGL working together.

Avoiding the DOM by using the canvas makes many apps fly and is the best bet for game / graphic intensive applications.

Tile map

This type of linear map map is best as an simple array (Tile map). It saves space, access is far quicker (including neighbours), and things like coordinates can be computed from the index.

Each element of the tile map holds the index into the terrain type. The world description object map holds an array of terrain type descriptions. To get the details for a tile you get the map item and use that as an index into the terrain description array.

eg if the world is 50 by 50 tiles and world.map is the tile map array. To get a terrain description for a tile

   function getTerrainDesc(x,y){
       const index = x + y * 50; // get the array index
       const terrainType = world.map[index]
       return world.terrainDesc[terrainType];
   }
   // see example for better examples of using a tile map.

Example

I have done a very quick rewrite of your map generator using some patterns you may not be comfortable with. Take it as an example only and use what you want, it is not the only way to do this type of thing.

I have provide various links in the code to MDN in regard to JS language features you may not know about. MDN is a good reference for all things browser and worth having a bookmark to.

The example generates a map using the same logic you used. The additional calls to createCanvas and drawWorld are needed.

You can define many worlds each with a different set of terrains and set ups. The example only uses one named world Click on the map to regenerate a new one (it uses the old map so not a completely new map)

// replacement for jQuery ready event
addEventListener("load",() => {
    world.container = document.querySelector(".world");  // get container
    createCanvas(world);
    genesis(world);
    terraform(world);
    drawWorld(world);
    world.canvas.addEventListener("click", () => {
       // comment the following to clear the world before regeneration.
       // world.map.fill(0);
       terraform(world);
       drawWorld(world);
    
    })

});

// some functionality for a cell
// const declaration see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const
const cell = {
    draw(x, y) { // x,y is map coord
        this.world.ctx.fillStyle = this.color;
        this.world.ctx.fillRect(x * this.width, y * this.height, this.width, this.height);
    }
}


// the ... is a spread operator link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
// set defaults for terrain types.
const terrains = {
    empty : {
        id : 0,  // type arrays are set to zero when created. This is the default cell type.
        odds : 0, 
        addedOdds : 0,
        color : "#fff",
        ...cell,
    },
    river : {
        id : 1,  // 
        odds : 1,  // default odds for the terrain 
        addedOdds : 2,  // additional odds if cell has river neighbour
        color : "#29b6f6",
        ...cell,     // add the default functions for this cell.
    },
    forest : {
        id : 2,
        odds : 2, 
        addedOdds : 4,
        color : "#1b5e20",
        ...cell,        
    },
    plain : {
        id : 3,
        odds : 3, 
        addedOdds : 4,
        color : "#66bb6a",
        ...cell,        
    },
    desert : {
        id : 4,
        odds : 1, 
        addedOdds : 4,
        color : "#ffee58",
        ...cell,        
    },
    mountain : {
        id : 5,
        odds : 1, 
        addedOdds : 4,
        color : "#b0bec5",
        ...cell,        
    },
}



const world = {          
    cells : { 
        width : 100,
        height : 100,
    },
    cell : {
        width : 5,
        height : 5,
    },
    terrains : "river,forest,plain,desert,mountain".split(","), // terrains to use with this world
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
Object.assign(world, { // using assign so that we can access the variable world
    terrainDesc : [
        {...terrains.empty }, // first one must always be empty
        // Now add the terrain types you want for this map
        ...world.terrains.map((name, i) => {
            return {
                ...terrains[name],   // make a copy of the terrain description
                id : i + 1,   // add custom details for this world
                ...world.cell, // add cell width and height for this world
                world,        // add a reference to the world so terrains can get details if needed
            }; 
        }),
    ],
});



function genesis(world) {
    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array
    world.map = new Uint8Array(world.cells.width * world.cells.height);
}
function createCanvas(world){
    // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D
    var canvas = document.createElement("canvas");
    canvas.width = world.cells.width * world.cell.width;
    canvas.height = world.cells.height * world.cell.height;
    canvas.ctx = canvas.getContext("2d");
    world.canvas = canvas;
    world.ctx = canvas.ctx;
    world.container.appendChild(canvas);
}
const selectTerrain = (() => {  // As a singleton. 
                                // This encasulates the functionality 
                                // so you dont polute the global scope.
    const tSelect = [];
    var tSelectBaseCount = 0;
    var neighbours, world, map, w, h;
    return { // the selectTerrain object 
        // next is a setter see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set
        set world(worldDesc) { 
            world = worldDesc;
            map = world.map;
            w = world.cells.width;
            h = world.cells.height;
            neighbours = [ // pre calculate neighbour coord and map array offet
                {x : -1, y : -1, offset : -w-1},
                {x :  0, y : -1, offset : -w},
                {x :  1, y : -1, offset : -w+1},
                {x : -1, y :  0, offset : -1},
                {x :  1, y :  0, offset :  1},
                {x : -1, y :  1, offset : w-1},
                {x :  0, y :  1, offset : w},
                {x :  1, y :  1, offset : w+1},
            ];
            tSelect.length = 0;
            tSelectBaseCount = 0;
            for (let i = 0; i < world.terrainDesc.length; i ++) {
                var odds = world.terrainDesc[i].odds;
                while(odds-- ){ tSelect.push(i) }
            }
            tSelectBaseCount = tSelect.length;
        },
        select(index) {
            const x = index % w;
            const y = index / w | 0; // the | 0 (bitwize OR 0 is the same as
                                     // Math.floor( ONLY use for positive numbers))
            var count = tSelectBaseCount;
            // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of
            for (const n of neighbours) { // n as n and prefix for neighbour
                if (x + n.x >= 0 && y + n.y >= 0 && x + n.x < w && y + n.y < h) {
                    var nType = map[index + n.offset];
                    var addOdds = world.terrainDesc[nType].addedOdds
                    while (addOdds--) { tSelect[count ++] = nType }
                }
            }
            map[index] = tSelect[Math.random() * count | 0]; // the | 0 (bitwize OR 0 is the
                                                             // same as Math.floor( 
                                                             // ONLY use for positive numbers))
        }
    }
})();

function terraform(world) {
    var i = 0;
    selectTerrain.world = world; // set up terrain selector
    while (i < world.map.length) { selectTerrain.select(i++) }
}

function drawWorld(world) {
    var ctx = world.ctx; // get the 2D render context
    ctx.clearRect(0, 0, world.canvas.width, world.canvas.height);
    var index = 0;
    var width = world.cells.width;
    var cellW = world.cell.width;
    var cellH = world.cell.width;
    while (index < world.map.length) {
        const terrainType = world.map[index];
        world.terrainDesc[terrainType]
           .draw(index % width | 0, (index++) / width | 0);
    }
}
.world {
    position: absolute;


    border: solid #a1a1a1 3px;
}
<div class="world"></div>

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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