Lab Notes

This was a big wee, we made a ton of changes, mostly to organize the code and make it easier to update and tweak the game mechanics and setup. We started by separating the main parts of the game into different files. We’ll go over those files and the changes we made in them.

First off, we need to add all these new files to the script tags in index.html.

<script src="p5.min.js"></script>
<script src="p5.play.js"></script>

<!-- game files -->
<script src="settings.js"></script>
<script src="assets.js"></script>
<script src="setup.js"></script>
<script src="scenes.js"></script>
<script src="game.js"></script>

settings.js

The settings.js file contains all the variables that are used to set up the game and create conditions for the gameplay. It started with a few global variables from the original game.js.

/*
    global variables
*/
var player;
var speed = 6;
var jump_speed = speed * 3;
var GRAVITY = 1;

We added a bunch of new variables that get used mostly in the setup.js file to build the game. Starting from the top here’s what got added.

/*
    global variables
*/
var speed = 6;
var GRAVITY = 1;
var gameWidth = 640;
var gameHeight = 360;

We already had speed and gravity for the speed of the game and the amount of gravity applied to the player. This adds gameWidth and gameHeight, which are used to make the game sprites position relative to the size of the canvas in case we want to update the canvas size later to make the game dimensions larger or smaller.

// player
var player;
var jump_speed = speed * 3;
var playerXStart = 80;
var playerYStart = 200;

Then we added the playerXStart and playerYStart to save the starting position of the player for when the game resets. We’re going to see a lot of variables with similar names.

// platform 
var platformXStart = playerXStart + 100;
var platformYStart = gameHeight - 100;
var numPlatforms = 3;
var platformYChange = 100;

The platform start position is set relative to the player and the game height. We can also set the number of platforms and the relative change in height for each new platform.

// scenery 
var numClouds = 3;
var cloudSpeed = speed * 0.05;
var cloudYMin = 50;
var cloudYMax = 150;

var numTrees = 10;
var treeSpeed = speed * 0.1;
var treeYMin = gameHeight - 50;
var treeYMax = gameHeight - 25;

These variables set up the non-interactive sprites which make up the game scenery. We can set the number of clouds and trees, the relative positions that are used to randomly generate them in setup and the speed at which they move across the screen.

cloudYMin and cloudYMax are used to set the range for the randomly generated coordinates, which we’ll see with other sprites as well.

random(cloudYMin, cloudYMax)
// obstacles
var numArrows = 1;
var arrowYMin = 150;
var arrowYMax = 250;
var arrowSpeed = speed * 2;

Finally the obstacles. We can set the number of arrows, the y position range and the speed.

assets.js

Next we have assets.js which doesn’t contain any new code. It’s just all the variables used for external assets (images or sound files) and the preload function, which will load them all.

// player animations
var idle_sheet, idle_animation;
var run_sheet, run_animation;
var jump_sheet, jump_animation;

// platform
var platforms;
var start_platform_img, platform_img;

// scenery
var clouds, cloud_sheet, cloud_animation;
var trees, tree_img;

// obstacles
var arrows; // group
var arrow_sheet, arrow_animation;

function preload() {
    idle_sheet = loadSpriteSheet("sprites/main_character/main_character_idle.png", 128, 128, 16);
    idle_animation = loadAnimation(idle_sheet);

    run_sheet = loadSpriteSheet("sprites/main_character/main_character_running.png", 128, 128, 6);
    run_animation = loadAnimation(run_sheet);

    jump_sheet = loadSpriteSheet("sprites/main_character/main_character_jumping.png", 128, 128, 30);
    jump_animation = loadAnimation(jump_sheet);

    platform_img = loadImage("sprites/scenery/platform.png");
    start_platform_img = loadImage("sprites/scenery/start_platform.png");

    cloud_sheet = loadSpriteSheet("sprites/scenery/cloud.png", 64, 32, 12);
    cloud_animation = loadAnimation(cloud_sheet);

    tree_img = loadImage("sprites/scenery/tree.png");

    arrow_sheet = loadSpriteSheet("sprites/obstacles/arrow.png", 32, 32, 3);
    arrow_animation = loadAnimation(arrow_sheet);
}

setup.js

The setup.js file has the setup function which uses a lot of the variables introduced in settings.js. It also has a new build function which does a lot of what setup did before so that we can reset the game if the player dies.

function setup() {
    createCanvas(gameWidth, gameHeight);

The createCanvas now uses the gameWidth and gameHeight from settings.js so the graphics and canvas can be controlled relatively.

	    // set up player/character
    player = createSprite(playerXStart, playerYStart);
    player.setCollider("rectangle", -5, 0, 55, 115);
    player.addAnimation("idle", idle_animation);
    player.addAnimation("run", run_animation);
    player.addAnimation("jump", jump_animation);
    player.isJumping = false;
    player.isGrounded = false;
    player.scale = 0.5;
    
    platforms = new Group();
    clouds = new Group();
    trees = new Group();
    arrows = new Group();
}

Player setup is the same, one new thing here is that the groups are all declared in one place so that they can be used multiple times.

The loops used to build each group are in the build function so they can be used more than once. setup can only be called once at the beginning of the program.

function build() {

    // set up platform
    var startPlatform = createSprite(platformXStart, platformYStart);
    startPlatform.setCollider("rectangle", 0, 0, 512, 32);
    startPlatform.addImage("default", start_platform_img);
    startPlatform.velocity.x = -speed;
    startPlatform.isStartPlatform = true;
    platforms.add(startPlatform);

The first platform uses the platformXStart and platformYStart and is otherwise the same.

    var y = platformYStart;
    for (var i = 0; i < numPlatforms; i++) {
        var x = 512 + 256 * i;
        var platform = createSprite(x, y, 128, 32);
        platform.addImage("default", platform_img);
        platform.velocity.x = -speed;
        platforms.add(platform);
        
        // adjust y
        y += random(-platformYChange, platformYChange);
    }

The other platforms now use settings.js variables to set the randomized y value.

    // set up scenery
    for (var i = 0; i < numClouds; i++) {
        var x = random(0, width);
        var y = random(cloudYMin, cloudYMax);
        var cloud = createSprite(x, y);
        cloud.addAnimation("default", cloud_animation);
        cloud.velocity.x = -random(cloudSpeed, cloudSpeed * 2);
        clouds.add(cloud);
    }

    
    for (var i = 0; i < numTrees; i ++) {
        var x = random(0, 640);
        var y = random(treeYMin, treeYMax);
        var tree = createSprite(x, y);
        tree.addImage("default", tree_img);
        tree.velocity.x = -treeSpeed - random(0, 0.1);
        trees.add(tree);
    }

The scenery also uses variables from settings.js.

    // loop - structure in JavaScript that repeats code
    for (var i = 0; i < numArrows; i++) { // happens 3 times
        var x = random(width, width * 3);
        var y = random(arrowYMin, arrowYMax);
        var arrow = createSprite(x, y);
        arrow.setCollider("rectangle", 0, 0, 20, 10);
//        arrow.debug = true;
        arrow.addAnimation("default", arrow_animation);
        arrow.velocity.x = -arrowSpeed - random(0, 1);
        arrows.add(arrow);
    }
}

Finally setting up the obstacles uses settings.js variables.

scenes.js

scenes.js is where we added the most new code. This file handles what scene is rendered for the player. We added a intro scene and restart scene. The main scene is the game scene.

var scene = "intro";

function draw() {
    if (scene == "intro") {
        intro();   
    } else if (scene == "game") {
        game();
    } else if (scene == "restart") {
        restart();
    }
}

We use a string variable scene to keep track of which scene it is. Then we use the draw function to tell p5 which scene to draw. Each scene is rendered with a function matching the string name of the scene.

function intro() {
    background("black");
    
    // text styling
    fill("white");
    textAlign(CENTER, CENTER);
    textFont("Comic Sans MS");
    textSize(50);
    
    // title 
    text("My First Game", width/2, height/2);
     // instructions
    text("Press Enter to Start", width/2, height/2 + 100);
}

The intro scene is the first scene a player will see. We use some p5 text to give instructions to the player.

function restart() {
    background("black");
    
    fill("white");
    textAlign(CENTER, CENTER);
    textFont("Comic Sans MS");
    textSize(50);
    
    text("You Died :(", width/2, height/2);
    text("Enter to Start Over", width/2, height/2 + 100);
}

The restart scene is almost identical to the intro scene. When the player dies we go here and the player can start over.

function died() {
    scene = "restart";

The died function is trigger whenever the player goes off the canvas or gets hit by an arrow. It is used to set the scene to "restart".

    // reset the player
    player.position.x = playerXStart;
    player.position.y = playerYStart;
    player.velocity.y = 0;
    

We also reset the player by setting the position back to the start position and setting the y velocity to 0.

    // destroy other sprites
    while (platforms.length > 0) {
        platforms[0].remove();
    }
    
    while (clouds.length > 0) {
        clouds[0].remove();
    }
    
    while (trees.length > 0) {
        trees[0].remove();
    }
    
    while (arrows.length > 0) {
        arrows[0].remove();
    }
}

Finally we destroy all the sprites that were created when the game started so we can start from scratch.

Any sprites that are not part of a group can be removed by just called .remove() on the sprite itself like platform.remove() or enemy.remove().

function keyPressed() {
	// enter key
    if (keyCode == 13) {
        if (scene == "intro" || scene == "restart") {
            build();
            scene = "game";
        }
    }
}

The last part of this file is a keyPressed event listener so that the game can be started when the user hits Enter.

First we check to make sure the Enter key was pressed with the conditional statment: if (keyCode == 13) { }. The character code for the Enter key is 13.

Then if the scene is either intro or restart we want to start the game, so we call the build() function to create all of the sprites and then change the scene to "game".

game.js

Our last file. This is mostly what used to be the draw function but the name has been changed to game.

function game() {
    background("white");

    // character movement
    
    // loop through all platforms
    player.isGrounded = false;
    for (var i = 0; i < platforms.length; i++) {
        var platform = platforms[i];
        if (player.collide(platform)) {
            player.isJumping = false;
            player.changeAnimation("run");
            player.velocity.y = 0;
            player.isGrounded = true;
        }
        
        // wrap around canvas
        if (platform.collider.right() < 0) {
            if (platform.isStartPlatform) {
                platform.remove();
            }
            platform.position.x = width + platform.width/2;
            platform.position.y += random(-50, 50);
        }
    }
    
    if (!player.isGrounded) {
        player.velocity.y += GRAVITY;
    }
    
    
    if (keyDown("space") && !player.isJumping) {
        player.changeAnimation("jump");
        player.velocity.y -= jump_speed;
        player.isJumping = true;
    }

Most of it is the same.

    // arrows hit player
    arrows.overlap(player, function (arrow) {
        died();
    });

The first big change is here. If the arrow hits the player, we call the died function to go to the restart scene.

 	// wrap arrows back to the beginning 
    for (var i = 0; i < arrows.length; i++) {
        if (arrows[i].position.x < -50) {
            arrows[i].position.x = random(width, width * 3);
        }
    }
    
    // wrap clouds
    for (var i = 0; i < clouds.length; i++) {
        if (clouds[i].position.x < -100) {
            clouds[i].position.x = random(width, width * 2);
        }
    }
    
    // wrap trees
    for (var i = 0; i < trees.length; i++) {
        if (trees[i].position.x < -100) {
            trees[i].position.x = random(width, width * 2);
        }
    }

    // player falls below the canvas
    if (player.position.y - player.height > height || player.position.x < -player.width) {
        died();
    }

Same here, if the player goes off the canvas area we call the died function. Much easier than resetting the player and everything else in both places.

    drawSprites();
}

And that’s it! It may be challenging to get all of these changes implemented in your project so give yourself time before the Midterm and come to me if you have any difficulties.