David Liu

Sandbox

About the Game

Sandbox is quite simply and literally a sandbox. You can adjust the size of pixels and resize the window to any dimensions, then start dropping in sand. There are a few things to choose from with each having unique interactions either on its own or with other objects. Fire will burn away sand and plants, while plants will grow across the space and prevent sand from falling. Create, change, and destroy anything you'd like within this simple game of creativity and experimentation.

Developed from January 2020 to February 2020.

Play the game here!

Role in Development

This sandbox game is a simple project that demonstrates my skills and knowledge of JavaScript and creating games in the web browser. Though I don't plan on usually making games in the web browser, I wanted to make sure that I had the capability of creating one as such. I developed the entirety of the simulation myself, creating the physics system, collision detection, and unique properties of each object within the expandable grid space.

Script Examples

Index.jsdxlLIB.js
'use strict';

let canvas;
let ctx;
let canvasWidth;
let canvasHeight;
let mouseDown = false;
let mouseXPos;
let mouseYPos;

let updateTime = 10;
//let walkerSpeed = 1000/12;
let thread;
let pixelWidth = 10;

let block = {
    x:0,
    y:0,
    typeIndex: 1,
    color: '#2e2e2e',
    width: pixelWidth,
    gravity: 0,
    update() {
        //worldGraph[this.x][this.y] = this.typeIndex;
    }
}

let sand = {
    x:0,
    y:0,
    typeIndex: 2,
    color: "white",
    width: pixelWidth,
    gravity: 1,
    slideRadius: 3,
    update() {
        // Check if not at bottom
        if (this.y + (this.gravity * pixelWidth) < canvasHeight) {
            // Check if able to fall
            if (dxlLIB.checkValidPosition(this.x, this.y + (this.gravity * pixelWidth), [0])) {
                worldGraph[this.x][this.y] = 0; // No longer occupying previous space
                this.y += this.gravity * pixelWidth;
                worldGraph[this.x][this.y] = this.typeIndex; // Update sand into new spot
            }
            // Check if should "slide" off to the left
            else if (dxlLIB.checkValidPosition(this.x - pixelWidth, this.y + (this.gravity * pixelWidth), [0]) &&
                     dxlLIB.checkValidPosition(this.x - pixelWidth, this.y, [0])) {
                worldGraph[this.x][this.y] = 0; // No longer occupying previous space
                this.x -= pixelWidth;
                worldGraph[this.x][this.y] = this.typeIndex; // Update sand into new spot
            }
            // Check if should "slide" off to the right
            else if (dxlLIB.checkValidPosition(this.x + pixelWidth, this.y + (this.gravity * pixelWidth), [0]) &&
                     dxlLIB.checkValidPosition(this.x + pixelWidth, this.y, [0])) {
                worldGraph[this.x][this.y] = 0; // No longer occupying previous space
                this.x += pixelWidth;
                worldGraph[this.x][this.y] = this.typeIndex; // Update sand into new spot
            }
        }
    }
}

let fire = {
    x:0,
    y:0,
    typeIndex: 3,
    color: "red",
    width: pixelWidth,
    gravity: -1,
    liveTime: 3,
    liveCounter: 0,
    update() {
        // Check if fire can spread
        // Left
        if (dxlLIB.checkValidPosition(this.x - pixelWidth, this.y, [2, 4, 5])) {
            let newObject = Object.assign({}, fire);
            newObject.x = this.x - pixelWidth;
            newObject.y = this.y;
            
            dxlLIB.replaceObject(newObject);
        }
        // Right
        if (dxlLIB.checkValidPosition(this.x + pixelWidth, this.y, [2, 4, 5])) {
            let newObject = Object.assign({}, fire);
            newObject.x = this.x + pixelWidth;
            newObject.y = this.y;
            
            dxlLIB.replaceObject(newObject);
        }
        // Up
        if (dxlLIB.checkValidPosition(this.x, this.y - pixelWidth, [2, 4, 5])) {
            let newObject = Object.assign({}, fire);
            newObject.x = this.x;
            newObject.y = this.y - pixelWidth;
            
            dxlLIB.replaceObject(newObject);
        }
        // Down
        if (dxlLIB.checkValidPosition(this.x, this.y + pixelWidth, [2, 4, 5])) {
            let newObject = Object.assign({}, fire);
            newObject.x = this.x;
            newObject.y = this.y + pixelWidth;
            
            dxlLIB.replaceObject(newObject);
        }
            
        
        // Check if not at top
        if (this.y + (this.gravity * pixelWidth) > 0) {
            // Check if able to rise
            if (dxlLIB.checkValidPosition(this.x, this.y + (this.gravity * pixelWidth), [0])) {
                worldGraph[this.x][this.y] = 0; // No longer occupying previous space
                this.y += this.gravity * pixelWidth;
                worldGraph[this.x][this.y] = this.typeIndex; // Update fire into new spot
                
                this.liveCounter++;
            } else
                this.liveCounter = this.liveTime;
        } else
            this.liveCounter = this.liveTime;
        
        // Check if dead
        if (this.liveCounter >= this.liveTime)
            dxlLIB.removeObject(this);
    }
}

let plant = {
    x:0,
    y:0,
    typeIndex: 4,
    color: "green",
    width: pixelWidth,
    gravity: 0,
    growChance: 0.01,
    growDir: -1,
    update() {
        if (this.growDir == -1) {
            // First randomize direction
            this.growDir = Math.round(Math.random() * 3);
        }
        // Then random chance of growing
        // Left
        else if (this.growDir == 0) {
            if (dxlLIB.checkValidPosition(this.x - pixelWidth, this.y, [0]) && 
                    Math.random() < this.growChance) {
                        let newObject = Object.assign({}, plant);
                        newObject.x = this.x - pixelWidth;
                        newObject.y = this.y;
                        worldObjects.push(newObject);
                        worldGraph[newObject.x][newObject.y] = newObject.typeIndex
                        this.growDir = -1;
            }
            else if (Math.random() < this.growChance) {
                // Can't grow anymore
                this.growDir = -1;
            }
        }
        // Right
        else if (this.growDir == 1) {
            if (dxlLIB.checkValidPosition(this.x + pixelWidth, this.y, [0]) &&
                    Math.random() < this.growChance) {
                        let newObject = Object.assign({}, plant);
                        newObject.x = this.x + pixelWidth;
                        newObject.y = this.y;
                        worldObjects.push(newObject);
                        worldGraph[newObject.x][newObject.y] = newObject.typeIndex;
                        this.growDir = -1;
            }
            else if (Math.random() < this.growChance) {
                // Can't grow anymore
                this.growDir = -1;
            }
        }
        // Up
        else if (this.growDir == 2) {
            if (dxlLIB.checkValidPosition(this.x, this.y - pixelWidth, [0]) &&
                    Math.random() < this.growChance) {
                        let newObject = Object.assign({}, plant);
                        newObject.x = this.x;
                        newObject.y = this.y - pixelWidth;
                        worldObjects.push(newObject);
                        worldGraph[newObject.x][newObject.y] = newObject.typeIndex;
                        this.growDir = -1;
            }
            else if (Math.random() < this.growChance) {
                // Can't grow anymore
                this.growDir = -1;
            }
        }
        // Down
        else if (this.growDir == 3) {
            if (dxlLIB.checkValidPosition(this.x, this.y + pixelWidth, [0]) &&
                    Math.random() < this.growChance) {
                        let newObject = Object.assign({}, plant);
                        newObject.x = this.x;
                        newObject.y = this.y + pixelWidth;
                        worldObjects.push(newObject);
                        worldGraph[newObject.x][newObject.y] = newObject.typeIndex;
                        this.growDir = -1;
            }
            else if (Math.random() < this.growChance) {
                // Can't grow anymore
                this.growDir = -1;
            }
        }
    }
}

let ball = {
    x:0,
    y:0,
    typeIndex: 5,
    color: dxlLIB.getRandomColor(),
    width: pixelWidth,
    gravity: 0,
    speed: 3,
    speedCounter: 0,
    direction: Math.round(Math.random() * 7),
    update() {
        // Up
        if (this.direction == 0) {
            // Check if at the top of the screen
            if (this.y - pixelWidth > 0) {
                // Check if hitting something
                if (dxlLIB.checkValidPosition(this.x, this.y - pixelWidth, [1, 2, 3, 4, 5])) {
                    dxlLIB.removeObject(worldGraph[this.x][this.y - pixelWidth]);
                    
                    // Reverse direction
                    this.direction = Math.round(Math.random() * 2) + 3;
                } else {
                    // Move up
                    worldGraph[this.x][this.y] = 0; // No longer occupying previous space
                    this.y -= pixelWidth;
                    worldGraph[this.x][this.y] = this.typeIndex; // Update ball into new spot
                }
            } else {
                // Reverse direction
                this.direction = Math.round(Math.random() * 2) + 3;
            }
        }
        // Up right
        else if (this.direction == 1) {
            // Check if at the top of the screen
            if (this.y - pixelWidth > 0 && this.x + pixelWidth < Math.round(canvasWidth / pixelWidth) * pixelWidth) {
                // Check if hitting something
                if (dxlLIB.checkValidPosition(this.x + pixelWidth, this.y - pixelWidth, [1, 2, 3, 4, 5])) {
                    dxlLIB.removeObject(worldGraph[this.x + pixelWidth][this.y - pixelWidth]);
                    
                    // Reverse direction
                    this.direction = Math.round(Math.random() * 2) + 4;
                } else {
                    // Move up right
                    worldGraph[this.x][this.y] = 0; // No longer occupying previous space
                    this.x += pixelWidth;
                    this.y -= pixelWidth;
                    worldGraph[this.x][this.y] = this.typeIndex; // Update ball into new spot
                }
            } else {
                // Reverse direction
                this.direction = Math.round(Math.random() * 2) + 4;
            }
        }
        // Right
        else if (this.direction == 2) {
            // Check if at the top of the screen
            if (this.x + pixelWidth < Math.round(canvasWidth / pixelWidth) * pixelWidth) {
                // Check if hitting something
                if (dxlLIB.checkValidPosition(this.x + pixelWidth, this.y, [1, 2, 3, 4, 5])) {
                    dxlLIB.removeObject(worldGraph[this.x + pixelWidth][this.y]);
                    
                    // Reverse direction
                    this.direction = Math.round(Math.random() * 2) + 5;
                } else {
                    // Move up right
                    worldGraph[this.x][this.y] = 0; // No longer occupying previous space
                    this.x += pixelWidth;
                    worldGraph[this.x][this.y] = this.typeIndex; // Update ball into new spot
                }
            } else {
                // Reverse direction
                this.direction = Math.round(Math.random() * 2) + 5;
            }
        }
        // Down right
        else if (this.direction == 3) {
            // Check if at the top of the screen
            if (this.y + pixelWidth < Math.round(canvasHeight / pixelWidth) * pixelWidth && this.x + pixelWidth < Math.round(canvasWidth / pixelWidth) * pixelWidth) {
                // Check if hitting something
                if (dxlLIB.checkValidPosition(this.x + pixelWidth, this.y + pixelWidth, [1, 2, 3, 4, 5])) {
                    dxlLIB.removeObject(worldGraph[this.x + pixelWidth][this.y + pixelWidth]);
                    
                    // Reverse direction
                    this.direction = Math.round(Math.random() * 2) + 6;
                    if (this.direction > 7) this.direction = this.direction - 8;
                } else {
                    // Move up right
                    worldGraph[this.x][this.y] = 0; // No longer occupying previous space
                    this.x += pixelWidth;
                    this.y += pixelWidth;
                    worldGraph[this.x][this.y] = this.typeIndex; // Update ball into new spot
                }
            } else {
                // Reverse direction
                this.direction = Math.round(Math.random() * 2) + 6;
                if (this.direction > 7) this.direction = this.direction - 8;
            }
        }
        // Down
        else if (this.direction == 4) {
            // Check if at the top of the screen
            if (this.y + pixelWidth < Math.round(canvasHeight / pixelWidth) * pixelWidth) {
                // Check if hitting something
                if (dxlLIB.checkValidPosition(this.x, this.y + pixelWidth, [1, 2, 3, 4, 5])) {
                    dxlLIB.removeObject(worldGraph[this.x][this.y + pixelWidth]);
                    
                    // Reverse direction
                    this.direction = Math.round(Math.random() * 2) + 7;
                    if (this.direction > 7) this.direction = this.direction - 8;
                } else {
                    // Move up right
                    worldGraph[this.x][this.y] = 0; // No longer occupying previous space
                    this.y += pixelWidth;
                    worldGraph[this.x][this.y] = this.typeIndex; // Update ball into new spot
                }
            } else {
                // Reverse direction
                this.direction = Math.round(Math.random() * 2) + 7;
                if (this.direction > 7) this.direction = this.direction - 8;
            }
        }
        // Down left
        else if (this.direction == 5) {
            // Check if at the top of the screen
            if (this.y + pixelWidth < Math.round(canvasHeight / pixelWidth) * pixelWidth && this.x - pixelWidth > 0) {
                // Check if hitting something
                if (dxlLIB.checkValidPosition(this.x - pixelWidth, this.y + pixelWidth, [1, 2, 3, 4, 5])) {
                    dxlLIB.removeObject(worldGraph[this.x - pixelWidth][this.y + pixelWidth]);
                    
                    // Reverse direction
                    this.direction = Math.round(Math.random() * 2);
                } else {
                    // Move up right
                    worldGraph[this.x][this.y] = 0; // No longer occupying previous space
                    this.x -= pixelWidth;
                    this.y += pixelWidth;
                    worldGraph[this.x][this.y] = this.typeIndex; // Update ball into new spot
                }
            } else {
                // Reverse direction
                this.direction = Math.round(Math.random() * 2);
            }
        }
        // Left
        else if (this.direction == 6) {
            // Check if at the top of the screen
            if (this.x - pixelWidth > 0) {
                // Check if hitting something
                if (dxlLIB.checkValidPosition(this.x - pixelWidth, this.y, [1, 2, 3, 4, 5])) {
                    dxlLIB.removeObject(worldGraph[this.x - pixelWidth][this.y]);
                    
                    // Reverse direction
                    this.direction = Math.round(Math.random() * 2) + 1;
                } else {
                    // Move up right
                    worldGraph[this.x][this.y] = 0; // No longer occupying previous space
                    this.x -= pixelWidth;
                    worldGraph[this.x][this.y] = this.typeIndex; // Update ball into new spot
                }
            } else {
                // Reverse direction
                this.direction = Math.round(Math.random() * 2) + 1;
            }
        }
        // Up left
        else if (this.direction == 7) {
            // Check if at the top of the screen
            if (this.y - pixelWidth > 0 && this.x - pixelWidth > 0) {
                // Check if hitting something
                if (dxlLIB.checkValidPosition(this.x - pixelWidth, this.y - pixelWidth, [1, 2, 3, 4, 5])) {
                    dxlLIB.removeObject(worldGraph[this.x - pixelWidth][this.y - pixelWidth]);
                    
                    // Reverse direction
                    this.direction = Math.round(Math.random() * 2) + 2;
                } else {
                    // Move up right
                    worldGraph[this.x][this.y] = 0; // No longer occupying previous space
                    this.x -= pixelWidth;
                    this.y -= pixelWidth;
                    worldGraph[this.x][this.y] = this.typeIndex; // Update ball into new spot
                }
            } else {
                // Reverse direction
                this.direction = Math.round(Math.random() * 2) + 2;
            }
        }
    }
}

let worldGraph = [];
let worldObjects = [];

let currentType;
let types = [];

    // #0 - in this class we will always use ECMAScript 5's "strict" mode
    // See what 'use strict' does here:
    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/Strict_mode
    // #1 call the init function after the pages loads
    window.onload = init;

    function init() {
        console.log("page loaded!");

        // Setup canvas
        canvas = document.querySelector('canvas');
        ctx = canvas.getContext('2d');
        
        window.addEventListener('resize', resizeCanvas, false);
        
        types.push(block);
        types.push(sand);
        types.push(fire);
        types.push(plant);
        types.push(ball);
        currentType = types[0];
        
        resizeCanvas();
        
        // Click event
        document.querySelector("canvas").onmousedown = function(e) {
            mouseDown = true;
            
            mouseXPos = e.pageX;
            mouseYPos = e.pageY;
        }
        
        document.querySelector("canvas").onmousemove = function(e) {
            // We only care when we need the mouse position
            if (mouseDown) {
                mouseXPos = e.pageX;
                mouseYPos = e.pageY;   
            }
        }
        
        document.querySelector("canvas").onmouseup = function(e) {
            mouseDown = false;
        }
        
        document.querySelector("#clear").onclick = function(e) {
            worldObjects = [];
            worldGraph = dxlLIB.createWorldGraph();
        }
        
        document.querySelector("#wall").onclick = function(e) {
            currentType = types[0];
            document.querySelector("#wall").className = "active";
            document.querySelector("#sand").className = "";
            document.querySelector("#fire").className = "";
            document.querySelector("#plant").className = "";
            document.querySelector("#ball").className = "";
        }
        
        document.querySelector("#sand").onclick = function(e) {
            currentType = types[1];
            document.querySelector("#wall").className = "";
            document.querySelector("#sand").className = "active";
            document.querySelector("#fire").className = "";
            document.querySelector("#plant").className = "";
            document.querySelector("#ball").className = "";
        }
        
        document.querySelector("#fire").onclick = function(e) {
            currentType = types[2];
            document.querySelector("#wall").className = "";
            document.querySelector("#sand").className = "";
            document.querySelector("#fire").className = "active";
            document.querySelector("#plant").className = "";
            document.querySelector("#ball").className = "";
        }
        
        document.querySelector("#plant").onclick = function(e) {
            currentType = types[3];
            document.querySelector("#wall").className = "";
            document.querySelector("#sand").className = "";
            document.querySelector("#fire").className = "";
            document.querySelector("#plant").className = "active";
            document.querySelector("#ball").className = "";
        }
        
        document.querySelector("#ball").onclick = function(e) {
            currentType = types[4];
            document.querySelector("#wall").className = "";
            document.querySelector("#sand").className = "";
            document.querySelector("#fire").className = "";
            document.querySelector("#plant").className = "";
            document.querySelector("#ball").className = "active";
        }
        
        let slider = document.querySelector("#pixelSlider");
        let output = document.querySelector("#pixelWidth");
        output.innerHTML = slider.value; // Display the default slider value

        // Update the current slider value (each time you drag the slider handle)
        slider.oninput = function() {
            output.innerHTML = this.value;
            pixelWidth = parseInt(slider.value); // Make sure it is an integer
            
            // Change sizes of objects
            for (let i = 0; i < types.length; i++) {
                types[i].width = pixelWidth;
            }
            
            // Clear the objects
            worldObjects = [];
            resizeCanvas();
        } 
    }

    // Resizing Source: https://stackoverflow.com/questions/4288253/html5-canvas-100-width-height-of-viewport
    function resizeCanvas() {
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;
        canvasWidth = canvas.width;
        canvasHeight = canvas.height;
        
        // Background
        ctx.fillStyle = 'black'; 
        ctx.fillRect(0,0,canvasWidth,canvasHeight); 
        
        // Reset the thread
        clearInterval(thread);
        
        // Make the world
        worldGraph = dxlLIB.createWorldGraph();
        
        // Update again
        thread = setInterval(drawUpdate, updateTime);
    }

    function drawUpdate(){
        // Clear every frame
        ctx.fillRect(0,0,canvasWidth,canvasHeight);
        
        // Draw in sand while mouse is held
        if (mouseDown) {
            // Check if valid spawning point
            dxlLIB.spawnObject(mouseXPos, mouseYPos);
        }
        
        ctx.save();
        
        // Fire updates first
        for (let i = 0; i < worldObjects.length; i++) {
            if (worldObjects[i].typeIndex == 3) {
                ctx.fillStyle = worldObjects[i].color;
                ctx.fillRect(worldObjects[i].x-worldObjects[i].width/2,worldObjects[i].y-worldObjects[i].width/2,worldObjects[i].width/2,worldObjects[i].width/2);
                worldObjects[i].update();
            }
        }
        
        // Rest of updates
        for (let i = 0; i < worldObjects.length; i++) {
            if (worldObjects[i].typeIndex != 3) {
                ctx.fillStyle = worldObjects[i].color;
                ctx.fillRect(worldObjects[i].x-worldObjects[i].width/2,worldObjects[i].y-worldObjects[i].width/2,worldObjects[i].width/2,worldObjects[i].width/2);
                worldObjects[i].update();
            }
        }
        
        ctx.restore();
    }