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