10
\$\begingroup\$

I've taken my previous block building game and added some performance improvements, and commented some things that needed to be commented for better explanation.

/** Here are few instructions before you start...
  * IMPORTANT (ON KHANACADEMY ONLY): When you make changes to the code, all tiles will be black.
  * Just hit the restart button to fix that.
  *
  * CONTROLS:
  * MOUSE - Move around the current selected tile.
  * LEFT MOUSE BUTTON - Add a tile to the screen.
  * RIGHT MOUSE BUTTON - Delete a tile from the screen.
  * CONTROL KEY - Change the block type forward.
  * SHIFT KEY - Change the block type backward.
  * WASD or LEFT RIGHT DOWN UP - Move around the map.
*/

/* Global program constants */
var CURSOR_FONT = createFont("monospace", 15);
var WHITE = color(255, 255, 255);
var SUN_WIDTH = 100;
var SUN_HEIGHT = 100;
var TILE_SIZE = 10;
var COLORS = [
    color(180, 120, 20),
    color(20, 150, 20),
    color(100, 100, 100),
    color(240, 200, 10),
    color(5, 44, 117),
    color(255, 255, 255),
    color(110, 70, 10),
    color(10, 210, 20),
];
var TILE_TYPES = [
    "Dirt",
    "Grass",
    "Stone",
    "Sand",
    "Water",
    "Snow",
    "Wood",
    "Leaves",
];

/* Colors for the sun and moon */
var skyObjectColor1 = color(194, 181, 33);
var skyObjectColor2 = color(255, 140, 0);

/* Variables concerning map movement */
var movingUp = false;
var movingDown = false;
var movingLeft = false;
var movingRight = false;

/* Variables that control the darkness of the sky */
var starVisibilityChange = 0;
var starVisibility = 15;
var skyDarknessChange = 0.25;
var skyDarkness = 125;

/* Array containing cloud data */
var cloudArray = [];

/* Current selected color */
var selectedColor = 0;

/* Array contaning all tile data */
var tileArray = [];

/* Array containing all star data */
var starArray = [];

/* Variables controlling the sun and moon's position */
var skyObjectYChange = -0.05;
var SKY_OBJ_X_POS = 189;
var skyObjectYPos = 200;

/* Populate the starArray */
var generateStars = function() {
    for(var d = 0; d <= round(random(100, 150)); d++) {
        starArray.push({
            xPos: random(0, 400),
            yPos: random(0, 400),
        });
    }
};

/* Draw stars in the sky */
var drawStars = function() {
    for(var s = starArray.length-1; s >= 0; s--) {
        var star = starArray[s];
        noStroke();
        fill(255, 255, 255, starVisibility);
        rect(star.xPos, star.yPos, 2, 2);
    }
};

/* Render the day and night cycle*/
var renderDayNightCycle = function() {
    if(skyObjectYPos >= 250) {
        starVisibilityChange = 1;
        skyObjectColor1 = color(122, 117, 117);
        skyObjectColor2 = color(71, 68, 68);
        drawStars();
    }
    if(skyObjectYPos < 250) {
        starVisibilityChange = 0;
        skyObjectColor1 = color(194, 181, 33);
        skyObjectColor2 = color(255, 140, 0);
    }
    noStroke();
    fill(skyObjectColor2);
    rect(SKY_OBJ_X_POS, skyObjectYPos, 45, 45);
    fill(skyObjectColor1);
    rect(SKY_OBJ_X_POS+5, skyObjectYPos+5, 35, 35);
    skyObjectYPos += skyObjectYChange;
    skyDarkness += skyDarknessChange;
    starVisibility += starVisibilityChange;
    if(skyObjectYPos <= -50) {
        skyObjectYChange = 0.05;
        skyDarknessChange = -0.05;
    }
    if(skyObjectYPos >= 450) {
        skyObjectYChange = -0.05;
        skyDarknessChange = 0.05;
        starVisibilityChange = -2.5;
    }
};

/* Generate new terrain for the world */
var generateTerrain = function() {
    var c = random(1, 3);

    /* Variable concerning the current Y position of the tile generator */
    var blockY = 300;

    /* Overarching for loop, this loop determines world size */
    for(var x = -2500; x <= 2500; x += TILE_SIZE) {

        /* Create part of the dirt section, with grass on top*/
        var rb = round(random(-1, 0));
        tileArray.push({
            xPos: x,
            yPos: blockY+TILE_SIZE*rb,
            colr: COLORS[1],
        });
        if(rb === -1) {
            tileArray.push({
                xPos: x,
                yPos: blockY,
                colr: COLORS[0],
            });
        }

        /* If this statement is true, then place a tree down */
        if(random() >= random()*random()/random()+random()) {
            var treeHeight = round(random(2, 6));

            /* Create the wood section of the tree */
            for(var h = blockY-TILE_SIZE; h >= blockY-TILE_SIZE*treeHeight; h -= TILE_SIZE) {
                tileArray.push({
                    xPos: x,
                    yPos: h,
                    colr: COLORS[6],
                });
            }

            /* Create the leaf top of the tree */
            for(var i = 0; i <= 1; i++) {
                tileArray.push({
                    xPos: x,
                    yPos: blockY-TILE_SIZE*(treeHeight+i)-TILE_SIZE,
                    colr: COLORS[7],
                });
            }

            /* Add two extra leaf blocks on the sides for a more realistic tree */
            tileArray.push({
                xPos: x+TILE_SIZE,
                yPos: blockY-TILE_SIZE*treeHeight-TILE_SIZE,
                colr: COLORS[7]
            });
            tileArray.push({
                xPos: x-TILE_SIZE,
                yPos: blockY-TILE_SIZE*treeHeight-TILE_SIZE,
                colr: COLORS[7]
            });
        }

        /* Create the last part of the dirt section */
        for(var y = blockY; y <= blockY+TILE_SIZE*round(random(2, 4)); y += TILE_SIZE) {
            tileArray.push({
                xPos: x,
                yPos: y+TILE_SIZE,
                colr: COLORS[0],
            });
        }

        /* Create the stone section */
        for(var y = blockY+TILE_SIZE*round(c); y <= blockY+TILE_SIZE*random(18, 22); y += TILE_SIZE) {
            tileArray.push({
                xPos: x,
                yPos: y+TILE_SIZE,
                colr: COLORS[2],
            });
        }

        /* Change the blockY by a random, but rounded amount */
        blockY += (ceil(random(-1, 1)/TILE_SIZE)*TILE_SIZE)*round(random(-2, 2));
    }
};

/* Initalize the array of clouds */
var generateClouds = function() {
    for(var i = 0; i <= round(random(2, 12)); i++) {
        cloudArray.push({
            xPos: random(50, 350),
            yPos: random(50, 150),
            w: random(30, 60),
            h: random(10, 20),
        });
    }
};

/* Draw the background */
var drawBackground = function() {
    for(var c = cloudArray.length-1; c >= 0; c--) {
        var cloud = cloudArray[c];
        noStroke();
        fill(WHITE);
        rect(cloud.xPos, cloud.yPos, cloud.w, cloud.h);
        cloud.xPos += random(0.01, 0.09);
        if(cloud.xPos >= 400) {
            cloud.xPos = 0-cloud.w;
        }
    }
};

/* Draw a hitbox over the selected position */
var drawHitbox = function(x, y) {
    noCursor();
    fill(WHITE);
    textFont(CURSOR_FONT);
    text("+", mouseX-1, mouseY+4);
    noFill();
    strokeWeight(1);
    stroke(WHITE);
    rect(x, y, TILE_SIZE, TILE_SIZE);
};

/* Add a block to the screen */
var addBlock = function(x, y) {
    drawHitbox(x, y);
    if(mouseIsPressed && mouseButton === LEFT) {
        if(x >= 20 && y >= 20) {
            tileArray.push({
                xPos: x,
                yPos: y,
                colr: COLORS[selectedColor],
            });
        }
    }
};

/* Delete a block from the screen */
var deleteBlock = function(x, y) {
    drawHitbox(x, y);
    if(mouseIsPressed && mouseButton === RIGHT) {
        for(var t = tileArray.length-1; t >= 0; t--) {
            var tile = tileArray[t];
            if(x === tile.xPos && y === tile.yPos) {
                tileArray.splice(t, 1);
            }
        }
    }
};

/* Render the tileArray */
var renderTiles = function() {
    for(var t = tileArray.length-1; t >= 0; t--) {
        var tile = tileArray[t];

        /* Check if tile is off screen, if so, don't render */ 
        if(tile.xPos >= 0 && tile.xPos <= 400 && tile.yPos >= 0 && tile.yPos <= 400) {
            noStroke();
            fill(tile.colr);
            rect(tile.xPos, tile.yPos, TILE_SIZE, TILE_SIZE);
        }
    }
};

/* Check for specific key actions */
var checkForKeyActions = function() {
    keyPressed = function() {
        if(keyCode === LEFT || keyCode === 65) {
            movingLeft = true;
        }
        if(keyCode === RIGHT || keyCode === 68) {
            movingRight = true;
        }
        if(keyCode === UP || keyCode === 87) {
            movingUp = true;
        }
        if(keyCode === DOWN || keyCode === 83) {
            movingDown = true;
        }
        if(keyCode === CONTROL) {
            selectedColor++;
            if(selectedColor >= COLORS.length) {
                selectedColor = 0;
            }
        }
        if(keyCode === SHIFT) {
            selectedColor--;
            if(selectedColor < 0) {
                selectedColor = COLORS.length-1;
            }
        }
    };

    /* If key released, change keypress variable to false */
    keyReleased = function() {
        if(keyCode === LEFT || keyCode === 65) {
            movingLeft = false;
        }
        if(keyCode === RIGHT || keyCode === 68) {
            movingRight = false;
        }
        if(keyCode === UP || keyCode === 87) {
            movingUp = false;
        }
        if(keyCode === DOWN || keyCode === 83) {
            movingDown = false;
        }
    };

    /* Move up */
    if(movingUp) {
        for(var t = tileArray.length-1; t >= 0; t--) {
            var tile = tileArray[t];
            tile.yPos += TILE_SIZE;
        }
    }

    /* Move down */
    if(movingDown) {
        for(var t = tileArray.length-1; t >= 0; t--) {
            var tile = tileArray[t];
            tile.yPos -= TILE_SIZE;
        }
    }

    /* Move left */
    if(movingLeft) {
        for(var t = tileArray.length-1; t >= 0; t--) {
            var tile = tileArray[t];
            tile.xPos += TILE_SIZE;
        }
    }

    /* Move right */
    if(movingRight) {
        for(var t = tileArray.length-1; t >= 0; t--) {
            var tile = tileArray[t];
            tile.xPos -= TILE_SIZE;
        }
    }
};

/* Draw the current selected tile */
var drawSelectedTile = function() {
    strokeWeight(1.5);
    stroke(255, 255, 255);
    fill(COLORS[selectedColor]);
    rect(5, 5, 15, 15);
    fill(255, 255, 255);
    textFont(CURSOR_FONT);
    text(TILE_TYPES[selectedColor], 25, 17.4); 
};

/* Load the world, clouds and stars before the draw loop begins */
generateTerrain();
generateClouds();
generateStars();

/* Main draw loop */
draw = function() {
    background(0, 0, skyDarkness);
    renderDayNightCycle();
    drawBackground();
    renderTiles();
    drawSelectedTile();
    checkForKeyActions();

    /* Check if the user wants to add a block */
    addBlock(ceil(mouseX/TILE_SIZE)*TILE_SIZE-TILE_SIZE, 
             ceil(mouseY/TILE_SIZE)*TILE_SIZE-TILE_SIZE);

    /* Check if the user wants to delete a block */
    deleteBlock(ceil(mouseX/TILE_SIZE)*TILE_SIZE-TILE_SIZE, 
                ceil(mouseY/TILE_SIZE)*TILE_SIZE-TILE_SIZE);

    /* Draw a hitbox over the current selected block */
    drawHitbox(ceil(mouseX/TILE_SIZE)*TILE_SIZE-TILE_SIZE, 
               ceil(mouseY/TILE_SIZE)*TILE_SIZE-TILE_SIZE);
};

What I'm mostly looking for is how I can improve the code that lets the player move around the world. Currently, what it's doing is looping through the entire array containing the world data, and changing each individual block's position. This is not good, especially when I'd like to make the worlds infinite at some point. So, any suggestions or solutions to fix/improve this are welcome. Thanks!

\$\endgroup\$
8
\$\begingroup\$

Your code style is good, but first and foremost, I want to talk about the two major logical problems with your code: randomness and camera control.


Randomness

You've mentioned that you have a desire to make the world infinite at some point. Well, if you want to do that, you're going to need to rework how you're currently handling random number generation.

In order to make the world infinite, you have to be able to generate chunks of the world lazily. When a new chunk is needed, it needs to be generated on-demand. Actual chunk generation and management is a whole topic in and of itself, but I expect you'll be able to tackle it. I'm just going to outline the changes you need to make to allow such a mechanism to be possible.

Your generateTerrain function is pretty big. It fills tileArray with all the data it needs for the game. You'll need to change that to something like generateChunk which will fill a chunkArray with new tiles. However, currently, that's not easy because of how you're generating random numbers.

/* Change the blockY by a random, but rounded amount */
blockY += (ceil(random(-1, 1)/TILE_SIZE)*TILE_SIZE)*round(random(-2, 2));

Here, you're using random, and you're adjusting the position as your iterate, incrementally. This is problematic for a number of reasons. First of all, random is entirely nondeterministic. There's no way to save a "seed" for this kind of world. You could implement some kind of system that would save the state on chunk borders, but not only would that cause problems, it would be unnecessarily difficult.

Fortunately, Processing provides a noise function. Along with noiseSeed and noiseDetail, you can create deterministic, continuous random numbers for any given x/y coordinate! This is obviously beneficial for a few reasons.

  1. Your generation function will become essentially stateless. Currently, you're incrementally adjusting things like blockY, which works, but it's a little hacky. Using noise, each individual block can be uniquely defined by a combination of a seed and an x/y pair—no state needed.
  2. Chunk boundaries will be continuous. This is a side-effect of the above point. Since Perlin noise (the type of noise Processing generates) is continuous, and since you're not keeping any state, you won't have to worry about handling boundaries.
  3. Tuning the generation will become simpler. Adjusting the noise function itself will help to adjust the output rather than worrying about individual nitty-gritty random number generations.

Clearly, using noise is the correct approach. But what about generating things like trees and other features? This can be done via noise as well, but using a differently-tuned noise function on a chunk-by-chunk basis. You could also use random in conjunction with randomSeed, and the seed itself could be a function of your noise! This allows chunk generation to be deterministic while sill being able to leverage the convenience of specific control over your generator. (Just remember that this will produce non-continuous data, so it can only be used for generating localized map features.)

So that's random number generation. Moving on to the simpler changes.


Moving Around

This is how you currently handle movement.

/* Move up */
if(movingUp) {
    for(var t = tileArray.length-1; t >= 0; t--) {
        var tile = tileArray[t];
        tile.yPos += TILE_SIZE;
    }
}

This is a terrible, terrible idea. Tile positions should never change. In fact, they don't even need to be stored with the tiles—they should be functions of their position within the tile array (or eventually, the chunk array).

No, the data itself should not move. Instead, you should have a "camera" that moves around the world, changing what is seen in the viewport. The simplest way to do this is using Processing's translate function. This will cause all drawing operations to be offset on the screen. Just call translate, do your drawing, then call translate again to undo the translation.

This will change how renderTiles and checkForKeyActions function. Instead of moving the tiles around, keep cameraX and cameraY variables (or camera.x/camera.y). Modify these in checkForKeyActions, then pass them to translate in renderTiles. This means no more looping through all the tiles just to move them, and it also means you can move continuously, not just on a grid!

Even with that implemented, if you make the world infinite, you obviously can't draw it all, since that would take an infinite amount of time. Instead, you'd want to calculate the chunks that intersect the viewport, and just draw those. If a chunk should be drawn, but it doesn't exist yet, that would be the time to generate it on the fly.


Odds and Ends

Besides those (admittedly major) points mentioned above, your code is fairly solid. Here's a few other random suggestions.

var COLORS = [
    color(180, 120, 20),
    color(20, 150, 20),
    color(100, 100, 100),
    color(240, 200, 10),
    color(5, 44, 117),
    color(255, 255, 255),
    color(110, 70, 10),
    color(10, 210, 20),
];
var TILE_TYPES = [
    "Dirt",
    "Grass",
    "Stone",
    "Sand",
    "Water",
    "Snow",
    "Wood",
    "Leaves",
];

Why not encapsulate these together? It would make more sense for TILE_TYPES to be a structure containing both pieces of information. For example, you could have tile.name and tile.color. This would allow you to add more information to tiles types in the future with relative ease.

var generateStars = function() {
    for(var d = 0; d <= round(random(100, 150)); d++) {
        starArray.push({
            xPos: random(0, 400),
            yPos: random(0, 400),
        });
    }
};

While this probably isn't a big deal, you could rewrite this function to use noise as well to make it more clear.

\$\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.