I'm a beginner using p5js and I'm trying to work with classes. I'm making a game where you have to find and click a 'wanted man', from a crowd.
So basically, a randomizer picks between 7 different types of 'civilians', and it's supposed to remove one of the types from the 'civilians' that have been spawned. After removing the 'wanted man', I want to add one wanted man so that there is only one 'wanted man'.
So the code spawns a bunch of random 'civilians', then it will delete all 'wanted man' types in the array, and add only one of them. I think there is a better way to do this though.
My basic desire is to have a crowd of 'civilians' that run around, - one of which is a 'wanted man' - and you would have to find and click that 'wanted man' (kind of like a hunting/assassination game).
This is the code for the sketch.js file:
var civilians = [];
var page = 0;
var man1img;
var man2img;
var man3img;
var man4img;
var man5img;
var man6img;
var aliemanimg;
var w;
var h;
var spawnCount = 14;
var wantedMan;
var randCiv;
function preload() {
man1img = loadImage("man1.png");
man2img = loadImage("man2.png");
man3img = loadImage("man3.png");
man4img = loadImage("man4.png");
man5img = loadImage("man5.png");
man6img = loadImage("man6.png");
aliemanimg = loadImage("alieman.png");
}
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
}
function setup() {
createCanvas(windowWidth, windowHeight);
imageMode(CENTER);
// wantedMan = round(random(0, 6));
wantedMan = 0;
for (var i = 0; i < spawnCount; i++) {
randCiv = round(random(0, 6));
w = random(windowWidth);
h = random(windowHeight);
civilians.push(new Civilian(w, h, wantedMan, randCiv));
console.log(wantedMan);
if (civilians[i].isWantedMan()) {
//OVER HERE \/
civilians.splice(i, 1);
}
}
civilians.push(new Civilian(w, h, wantedMan, wantedMan));
}
// page setup
// page 1 : main screen (play, settings, and those stuff)
// page 2 : show chosen civilian
// page 3 : playing
// page 4 : lose
// page 5 : options
function draw() {
background(220, 80, 80);
for (var i = civilians.length - 1; i >= 0; i--) {
civilians[i].update();
civilians[i].show(mouseX, mouseY);
if (civilians[i].clickedOn(mouseX, mouseY)) {
// detect if is right person
console.log("clicked on boi");
if (civilians[i].isWantedMan()) {
console.log("HES WANTED");
} else {
console.log("HES NOT WANTED");
}
}
}
text(round(frameRate()), 20, 20);
//show wanted man
var tempImg = man1img;
if (wantedMan == 1) {
tempImg = man2img;
} else if (wantedMan == 2) {
tempImg = man3img;
} else if (wantedMan == 3) {
tempImg = man4img;
}
if (wantedMan == 4) {
tempImg = man5img;
} else if (wantedMan == 5) {
tempImg = man6img;
} else if (wantedMan == 6) {
tempImg = aliemanimg;
}
image(tempImg, 50, 70, 70, 90);
}
This is the code for the class:
class Civilian {
constructor(x, y, wantedMan, type) {
this.x = x;
this.y = y;
this.w = 47;
this.h = 60;
this.t = {
x: x,
y: y,
};
this.size = 47;
this.moveSpeed = 0.01;
this.moveDist = 20;
this.wantedMan = wantedMan;
this.civilian = type
this.civilianImg = man1img
this.wantedMan = wantedMan
}
update() {
//move target to random position
this.t.x = random(this.t.x - this.moveDist, this.t.x + this.moveDist);
this.t.y = random(this.t.y - this.moveDist, this.t.y + this.moveDist);
//edge detect
if (this.t.x < 0) {
this.t.x += 5;
}
if (this.t.x > width) {
this.t.x -= 5;
}
if (this.t.y < 0) {
this.t.y += 5;
}
if (this.t.y > height) {
this.t.y -= 5;
}
//images position follows target but with easing
this.x += (this.t.x - this.x) * this.moveSpeed;
this.y += (this.t.y - this.y) * this.moveSpeed;
}
show(ex, ey) {
var d = dist(ex, ey, this.x, this.y);
if (d > this.size / 2) {
tint(255, 255, 255);
} else {
tint(0, 255, 0);
}
if(this.civilian == 1) {
this.civilianImg = man2img
} else if(this.civilian == 2) {
this.civilianImg = man3img
} else if(this.civilian ==3) {
this.civilianImg = man4img
} if(this.civilian == 4) {
this.civilianImg = man5img
} else if(this.civilian == 5) {
this.civilianImg = man6img
} else if(this.civilian == 6) {
this.civilianImg = aliemanimg
}
image(this.civilianImg, this.x, this.y, 47, 60);
}
clickedOn(ex, ey) {
var d = dist(ex, ey, this.x, this.y);
return d < this.size / 2 && mouseIsPressed;
}
isWantedMan() {
return this.civilian == this.wantedMan;
}
}
However, whenever I add a .splice(i,1) under the 'for' loop in setup function - to remove the 'wanted man', it shows this error:
"TypeError: Cannot read properties of undefined (reading
'isWantedMan') at /sketch.js:41:22".
isWantedMan() is a function in the Civilian Class, that returns true if the current 'civilian' is wanted. The .splice is supposed to remove a object from the array, when it is a 'wanted man'.
I don't know why this happens. When I replace the .splice code with a console.log() code, then there is no error.
Also there were probably a lot of things that I could have done better in the code.
I have an object rendered to a canvas. I'm trying to get the object to move along a set path on a loop. Here is what I have:
// Canvas Element
var canvas = null;
// Canvas Draw
var ctx = null;
// Static Globals
var tileSize = 16,
mapW = 10,
mapH = 10;
// Instances of entities
var entities = [
// A single entity that starts at tile 28, and uses the setPath() function
{
id: 0,
tile: 28,
xy: tileToCoords(28),
width: 16,
height: 24,
speedX: 0,
speedY: 0,
logic: {
func: 'setPath',
// These are the parameters that go into the setPath() function
data: [0, ['down', 'up', 'left', 'right'], tileToCoords(28), 0]
},
dir: {up:false, down:false, left:false, right:false}
}
];
// Array for tile data
var map = [];
window.onload = function(){
// Populate the map array with a blank map and 4 walls
testMap();
canvas = document.getElementById('save');
ctx = canvas.getContext("2d");
// Add all the entities to the map array and start their behavior
for(var i = 0; i < entities.length; ++i){
map[entities[i].tile].render.object = entities[i].id;
if(entities[i].logic){
window[entities[i].logic.func].apply(null, entities[i].logic.data);
}
}
drawGame(map);
window.requestAnimationFrame(function(){
mainLoop();
});
};
function drawGame(map){
ctx.clearRect(0, 0, canvas.width, canvas.height);
// We save all the entity data for later so the background colors don't get rendered on top
var tileObjData = [];
for(var y = 0; y < mapH; ++y){
for(var x = 0; x < mapW; ++x){
var currentPos = ((y*mapW)+x);
ctx.fillStyle = map[currentPos].render.base;
ctx.fillRect(x*tileSize, y*tileSize, tileSize, tileSize);
var thisObj = map[currentPos].render.object;
if(thisObj !== false){
thisObj = entities[thisObj];
var originX = thisObj.xy.x;
var originY = thisObj.xy.y;
tileObjData.push(
{
id: thisObj.id,
originX: originX,
originY: originY,
width: thisObj.width,
height: thisObj.height,
}
);
}
}
}
// Draw all the entities after the background tiles are drawn
for(var i = 0; i < tileObjData.length; ++i){
drawEntity(tileObjData[i].id, tileObjData[i].originX, tileObjData[i].originY, tileObjData[i].width, tileObjData[i].height);
}
}
// Draws the entity data
function drawEntity(id, posX, posY, sizeX, sizeY){
var offX = posX + entities[id].speedX;
var offY = posY + entities[id].speedY;
ctx.fillStyle = '#00F';
ctx.fillRect(offX, offY + sizeX - sizeY, sizeX, sizeY);
entities[id].xy.x = offX;
entities[id].xy.y = offY;
}
// Redraws the canvas with the browser framerate
function mainLoop(){
drawGame(map);
for(var i = 0; i < entities.length; ++i){
animateMove(i, entities[i].dir.up, entities[i].dir.down, entities[i].dir.left, entities[i].dir.right);
}
window.requestAnimationFrame(function(){
mainLoop();
});
}
// Sets the speed, direction, and collision detection of an entity
function animateMove(id, up, down, left, right){
var prevTile = entities[id].tile;
if(up){
var topLeft = {x: entities[id].xy.x, y: entities[id].xy.y};
var topRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y};
if(!map[coordsToTile(topLeft.x, topLeft.y - 1)].state.passable || !map[coordsToTile(topRight.x, topRight.y - 1)].state.passable){
entities[id].speedY = 0;
}
else{
entities[id].speedY = -1;
}
}
else if(down){
var bottomLeft = {x: entities[id].xy.x, y: entities[id].xy.y + entities[id].width - 1};
var bottomRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y + entities[id].width - 1};
if(!map[coordsToTile(bottomLeft.x, bottomLeft.y + 1)].state.passable || !map[coordsToTile(bottomRight.x, bottomRight.y + 1)].state.passable){
entities[id].speedY = 0;
}
else{
entities[id].speedY = 1;
}
}
else{
entities[id].speedY = 0;
}
if(left){
var bottomLeft = {x: entities[id].xy.x, y: entities[id].xy.y + entities[id].width - 1};
var topLeft = {x: entities[id].xy.x, y: entities[id].xy.y};
if(!map[coordsToTile(bottomLeft.x - 1, bottomLeft.y)].state.passable || !map[coordsToTile(topLeft.x - 1, topLeft.y)].state.passable){
entities[id].speedX = 0;
}
else{
entities[id].speedX = -1;
}
}
else if(right){
var bottomRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y + entities[id].width - 1};
var topRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y};
if(!map[coordsToTile(bottomRight.x + 1, bottomRight.y)].state.passable || !map[coordsToTile(topRight.x + 1, topRight.y)].state.passable){
entities[id].speedX = 0;
}
else{
entities[id].speedX = 1;
}
}
else{
entities[id].speedX = 0;
}
entities[id].tile = coordsToTile(entities[id].xy.x + (entities[id].width / 2), entities[id].xy.y + (tileSize / 2));
map[entities[id].tile].render.object = id;
if(prevTile !== entities[id].tile){
map[prevTile].render.object = false;
}
}
//////////////////////////////////////
// THIS IS WHERE I'M HAVING TROUBLE //
//////////////////////////////////////
// A function that can be used by an entity to move along a set path
// id = The id of the entity using this function
// path = An array of strings that determine the direction of movement for a single tile
// originPoint = Coordinates of the previous tile this entity was at. This variable seems to be where problems happen with this logic. It should get reset for every tile length moved, but it only gets reset once currently.
// step = The current index of the path array
function setPath(id, path, originPoint, step){
// Determine if the entity has travelled one tile from the origin
var destX = Math.abs(entities[id].xy.x - originPoint.x);
var destY = Math.abs(entities[id].xy.y - originPoint.y);
if(destX >= tileSize || destY >= tileSize){
// Go to the next step in the path array
step = step + 1;
if(step >= path.length){
step = 0;
}
// Reset the origin to the current tile coordinates
originPoint = entities[id].xy;
}
// Set the direction based on the current index of the path array
switch(path[step]) {
case 'up':
entities[id].dir.up = true;
entities[id].dir.down = false;
entities[id].dir.left = false;
entities[id].dir.right = false;
break;
case 'down':
entities[id].dir.up = false;
entities[id].dir.down = true;
entities[id].dir.left = false;
entities[id].dir.right = false;
break;
case 'left':
entities[id].dir.up = false;
entities[id].dir.down = false;
entities[id].dir.left = true;
entities[id].dir.right = false;
break;
case 'right':
entities[id].dir.up = false;
entities[id].dir.down = false;
entities[id].dir.left = false;
entities[id].dir.right = true;
break;
};
window.requestAnimationFrame(function(){
setPath(id, path, originPoint, step);
});
}
// Take a tile index and return x,y coordinates
function tileToCoords(tile){
var yIndex = Math.floor(tile / mapW);
var xIndex = tile - (yIndex * mapW);
var y = yIndex * tileSize;
var x = xIndex * tileSize;
return {x:x, y:y};
}
// Take x,y coordinates and return a tile index
function coordsToTile(x, y){
var tile = ((Math.floor(y / tileSize)) * mapW) + (Math.floor(x / tileSize));
return tile;
}
// Generate a map array with a blank map and 4 walls
function testMap(){
for(var i = 0; i < (mapH * mapW); ++i){
// Edges
if (
// top
i < mapW ||
// left
(i % mapW) == 0 ||
// right
((i + 1) % mapW) == 0 ||
// bottom
i > ((mapW * mapH) - mapW)
) {
map.push(
{
id: i,
render: {
base: '#D35',
object: false,
sprite: false
},
state: {
passable: false
}
},
);
}
else{
// Grass
map.push(
{
id: i,
render: {
base: '#0C3',
object: false,
sprite: false
},
state: {
passable: true
}
},
);
}
}
}
<!DOCTYPE html>
<html>
<head>
<style>
body{
background-color: #000;
display: flex;
align-items: center;
justify-content: center;
color: #FFF;
font-size: 18px;
padding: 0;
margin: 0;
}
main{
width: 100%;
max-width: 800px;
margin: 10px auto;
display: flex;
align-items: flex-start;
justify-content: center;
flex-wrap: wrap;
}
.game{
width: 1000px;
height: 1000px;
position: relative;
}
canvas{
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-crisp-edges;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
.game canvas{
position: absolute;
top: 0;
left: 0;
width: 800px;
height: 800px;
}
</style>
</head>
<body>
<main>
<div class="game">
<canvas id="save" width="200" height="200" style="z-index: 1;"></canvas>
</div>
</main>
</body>
</html>
The problem is with the setPath() function, and more specifically I think it's something with the originPoint variable. The idea is that setPath() moves the object one tile per path string, and originPoint should be the coordinates of the last tile visited (so it should only get updated once the object coordinates are one tile length away from the originPoint). Right now it only gets updated the first time and then stops. Hopefully someone can point out what I got wrong here.
Your condition to change the path direction I change it to have conditions for each direction, something like:
if ((entities[id].dir.left && entities[id].xy.x <= tileSize) ||
(entities[id].dir.right && entities[id].xy.x >= tileSize*8) ||
(entities[id].dir.up && entities[id].xy.y <= tileSize) ||
(entities[id].dir.down && entities[id].xy.y >= tileSize*8)) {
and the originPoint was just a reference you should do:
originPoint = JSON.parse(JSON.stringify(entities[id].xy));
See the working code below
// Canvas Element
var canvas = null;
// Canvas Draw
var ctx = null;
// Static Globals
var tileSize = 16,
mapW = 10,
mapH = 10;
// Instances of entities
var entities = [
// A single entity that starts at tile 28, and uses the setPath() function
{
id: 0,
tile: 28,
xy: tileToCoords(28),
width: 16,
height: 24,
speedX: 0,
speedY: 0,
logic: {
func: 'setPath',
// These are the parameters that go into the setPath() function
data: [0, ['down', 'left', 'down', 'left', 'up', 'left', 'left', 'right', 'up', 'right', 'down','right', "up"], tileToCoords(28), 0]
},
dir: {up:false, down:false, left:false, right:false}
}
];
// Array for tile data
var map = [];
window.onload = function(){
// Populate the map array with a blank map and 4 walls
testMap();
canvas = document.getElementById('save');
ctx = canvas.getContext("2d");
// Add all the entities to the map array and start their behavior
for(var i = 0; i < entities.length; ++i){
map[entities[i].tile].render.object = entities[i].id;
if(entities[i].logic){
window[entities[i].logic.func].apply(null, entities[i].logic.data);
}
}
drawGame(map);
window.requestAnimationFrame(function(){
mainLoop();
});
};
function drawGame(map){
ctx.clearRect(0, 0, canvas.width, canvas.height);
// We save all the entity data for later so the background colors don't get rendered on top
var tileObjData = [];
for(var y = 0; y < mapH; ++y){
for(var x = 0; x < mapW; ++x){
var currentPos = ((y*mapW)+x);
ctx.fillStyle = map[currentPos].render.base;
ctx.fillRect(x*tileSize, y*tileSize, tileSize, tileSize);
var thisObj = map[currentPos].render.object;
if(thisObj !== false){
thisObj = entities[thisObj];
var originX = thisObj.xy.x;
var originY = thisObj.xy.y;
tileObjData.push(
{
id: thisObj.id,
originX: originX,
originY: originY,
width: thisObj.width,
height: thisObj.height,
}
);
}
}
}
// Draw all the entities after the background tiles are drawn
for(var i = 0; i < tileObjData.length; ++i){
drawEntity(tileObjData[i].id, tileObjData[i].originX, tileObjData[i].originY, tileObjData[i].width, tileObjData[i].height);
}
}
// Draws the entity data
function drawEntity(id, posX, posY, sizeX, sizeY){
var offX = posX + entities[id].speedX;
var offY = posY + entities[id].speedY;
ctx.fillStyle = '#00F';
ctx.fillRect(offX, offY + sizeX - sizeY, sizeX, sizeY);
entities[id].xy.x = offX;
entities[id].xy.y = offY;
}
// Redraws the canvas with the browser framerate
function mainLoop(){
drawGame(map);
for(var i = 0; i < entities.length; ++i){
animateMove(i, entities[i].dir.up, entities[i].dir.down, entities[i].dir.left, entities[i].dir.right);
}
window.requestAnimationFrame(function(){
mainLoop();
});
}
// Sets the speed, direction, and collision detection of an entity
function animateMove(id, up, down, left, right){
var prevTile = entities[id].tile;
if(up){
var topLeft = {x: entities[id].xy.x, y: entities[id].xy.y};
var topRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y};
if(!map[coordsToTile(topLeft.x, topLeft.y - 1)].state.passable || !map[coordsToTile(topRight.x, topRight.y - 1)].state.passable){
entities[id].speedY = 0;
}
else{
entities[id].speedY = -1;
}
}
else if(down){
var bottomLeft = {x: entities[id].xy.x, y: entities[id].xy.y + entities[id].width - 1};
var bottomRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y + entities[id].width - 1};
if(!map[coordsToTile(bottomLeft.x, bottomLeft.y + 1)].state.passable || !map[coordsToTile(bottomRight.x, bottomRight.y + 1)].state.passable){
entities[id].speedY = 0;
}
else{
entities[id].speedY = 1;
}
}
else{
entities[id].speedY = 0;
}
if(left){
var bottomLeft = {x: entities[id].xy.x, y: entities[id].xy.y + entities[id].width - 1};
var topLeft = {x: entities[id].xy.x, y: entities[id].xy.y};
if(!map[coordsToTile(bottomLeft.x - 1, bottomLeft.y)].state.passable || !map[coordsToTile(topLeft.x - 1, topLeft.y)].state.passable){
entities[id].speedX = 0;
}
else{
entities[id].speedX = -1;
}
}
else if(right){
var bottomRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y + entities[id].width - 1};
var topRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y};
if(!map[coordsToTile(bottomRight.x + 1, bottomRight.y)].state.passable || !map[coordsToTile(topRight.x + 1, topRight.y)].state.passable){
entities[id].speedX = 0;
}
else{
entities[id].speedX = 1;
}
}
else{
entities[id].speedX = 0;
}
entities[id].tile = coordsToTile(entities[id].xy.x + (entities[id].width / 2), entities[id].xy.y + (tileSize / 2));
map[entities[id].tile].render.object = id;
if(prevTile !== entities[id].tile){
map[prevTile].render.object = false;
}
}
//////////////////////////////////////
// THIS IS WHERE I'M HAVING TROUBLE //
//////////////////////////////////////
// A function that can be used by an entity to move along a set path
// id = The id of the entity using this function
// path = An array of strings that determine the direction of movement for a single tile
// originPoint = Coordinates of the previous tile this entity was at. This variable seems to be where problems happen with this logic. It should get reset for every tile length moved, but it only gets reset once currently.
// step = The current index of the path array
function setPath(id, path, originPoint, step){
if ((entities[id].dir.left && entities[id].xy.x <= originPoint.x - tileSize) ||
(entities[id].dir.right && entities[id].xy.x >= originPoint.x + tileSize) ||
(entities[id].dir.up && entities[id].xy.y <= originPoint.y - tileSize) ||
(entities[id].dir.down && entities[id].xy.y >= originPoint.y + tileSize)) {
// Go to the next step in the path array
step = step + 1;
if(step >= path.length){
step = 0;
}
// Reset the origin to the current tile coordinates
originPoint = JSON.parse(JSON.stringify(entities[id].xy));
}
// Set the direction based on the current index of the path array
switch(path[step]) {
case 'up':
entities[id].dir.up = true;
entities[id].dir.down = false;
entities[id].dir.left = false
entities[id].dir.right = false;
break;
case 'down':
entities[id].dir.up = false;
entities[id].dir.down = true;
entities[id].dir.left = false;
entities[id].dir.right = false;
break;
case 'left':
entities[id].dir.up = false;
entities[id].dir.down = false;
entities[id].dir.left = true;
entities[id].dir.right = false;
break;
case 'right':
entities[id].dir.up = false;
entities[id].dir.down = false;
entities[id].dir.left = false;
entities[id].dir.right = true;
break;
};
window.requestAnimationFrame(function(){
setPath(id, path, originPoint, step);
});
}
// Take a tile index and return x,y coordinates
function tileToCoords(tile){
var yIndex = Math.floor(tile / mapW);
var xIndex = tile - (yIndex * mapW);
var y = yIndex * tileSize;
var x = xIndex * tileSize;
return {x:x, y:y};
}
// Take x,y coordinates and return a tile index
function coordsToTile(x, y){
var tile = ((Math.floor(y / tileSize)) * mapW) + (Math.floor(x / tileSize));
return tile;
}
// Generate a map array with a blank map and 4 walls
function testMap(){
for(var i = 0; i < (mapH * mapW); ++i){
// Edges
if (
// top
i < mapW ||
// left
(i % mapW) == 0 ||
// right
((i + 1) % mapW) == 0 ||
// bottom
i > ((mapW * mapH) - mapW)
) {
map.push(
{
id: i,
render: {
base: '#D35',
object: false,
sprite: false
},
state: {
passable: false
}
},
);
}
else{
// Grass
map.push(
{
id: i,
render: {
base: '#0C3',
object: false,
sprite: false
},
state: {
passable: true
}
},
);
}
}
}
<!DOCTYPE html>
<html>
<head>
<style>
body{
background-color: #000;
display: flex;
align-items: center;
justify-content: center;
color: #FFF;
font-size: 18px;
padding: 0;
margin: 0;
}
main{
width: 100%;
max-width: 800px;
margin: 10px auto;
display: flex;
align-items: flex-start;
justify-content: center;
flex-wrap: wrap;
}
.game{
width: 1000px;
height: 1000px;
position: relative;
}
canvas{
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-crisp-edges;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
.game canvas{
position: absolute;
top: 0;
left: 0;
width: 800px;
height: 800px;
}
</style>
</head>
<body>
<main>
<div class="game">
<canvas id="save" width="200" height="200" style="z-index: 1;"></canvas>
</div>
</main>
</body>
</html>
As someone has already solved your bug...
This is more than a solution to your problem as the real problem you are facing is complexity, long complex if statements using data structures representing the same information in different ways making it difficult to see simple errors in logic.
On top of that you have some poor style habits that compound the problem.
A quick fix will just mean you will be facing the next problem sooner. You need to write in a ways that reduces the chances of logic errors due to increasing complexity
Style
First style. Good style is very important
Don't assign null to declared variables. JavaScript should not need to use null, the exception to the rule is that some C++ coders infected the DOM API with null returns because they did not understand JavaScipt (or was a cruel joke), and now we are stuck with null
window is the default this (global this) and is seldom needed. Eg window.requestAnimationFrame is identical to just requestAnimationFrame and window.onload is identical to onload
Don't pollute your code with inaccurate, redundant and/or obvious comments, use good naming to provide the needed information. eg:
var map[]; has the comment // array of tile data Well really its an array that has data, who would have guessed, so the comment can be // tiles but then map is a rather ambiguous name. Remove the comment and give the variable a better name.
The comment // Static Globals above some vars. Javascript does not have static as a type so the comment is wrong and the "global's" part is "duh..."
Use const to declare constants, move all the magic numbers to the top and define them as named const. A name has meaning, an number in some code has next to no meaning.
Don't assign listener to the event name, it is unreliable and can be hijacked or overwritten. Always use addEventListener to assign an event listener
Be very careful with your naming. eg the function named coordsToTile is confusing as it does not return a tile, it returns a tile index, either change the function name to match the functions behavior, or change the behavior to match the name.
Don't use redundant intermediate functions, examples:
Your frame request requestAnimationFrame(function(){mainLoop()}); should skip the middle man and be requestAnimationFrame(mainLoop);
You use Function.apply to call the function window[entities[i].logic.func].apply(null, entities[i].logic.data);. apply is used to bind context this to the call, you don't use this in the function so you don't need use the apply. eg window[entities[i].logic.func](...entities[i].logic.data);
BTW being forced to use bracket notation to access a global is a sign of poor data structure. You should never do that.
JavaScript has an unofficial idiomatic styles, you should try to write JS in this style. Some examples from your code
else on the same line as closing }
Space after if, else, for, function() and befor else, opening block {
An id and an index are not the same, use idx or index for an index and id for an identifier
Keep it simple
The more complex you make your data structures the harder it is for you to maintain them.
Structured
Define objects to encapsulate and organize your data.
A global config object, that is transprotable ie can converted be to and from JSON. it contains all the magic numbers, defaults, type descriptions, and what not needed in the game.
Create a set of global utilities that do common repeated tasks, ie create coordinates, list of directions.
Define object that encapsulate the settings and behaviors specific only to that object.
Use polymorphic object design, meaning that different objects use named common behaviors and properties. In the example all drawable object have a function called draw that takes an argument ctx, all objects that can be updated have a function called update
Example
This example is a complete rewrite of your code and fixing your problem. It may be a little advanced, but it is only an example to look though an pick up some tips.
A quick description of the objects used.
Objects
config is transportable config data
testMap is an example map description
tileMap does map related stuff
Path Object encapsulating path logic
Entity Object a single moving entity
Tile Object representing a single tile
game The game state manager
Games have states, eg loading, intro, inPlay, gameOver etc. If you do not plan ahead and create a robust state manager you will find it very difficult to move from one state to the next
I have included the core of a finite state manager. The state manager is responsible for updating and rendering. it is also responsible for all state changes.
setTimeout(() => game.state = "setup", 0); // Will start the game
const canvas = document.getElementById('save');
const ctx = canvas.getContext("2d");
const point = (x = 0, y = 0) => ({x,y});
const dirs = Object.assign(
[point(0, -1), point(1), point(0,1), point(-1)], { // up, right, down, left
"u": 0, // defines index for direction string characters
"r": 1,
"d": 2,
"l": 3,
strToDirIdxs(str) { return str.toLowerCase().split("").map(char => dirs[char]) },
}
);
const config = {
pathIdx: 28,
pathTypes: {
standard: "dulr",
complex: "dulrldudlruldrdlrurdlurd",
},
tile: {size: 16},
defaultTileName: "grass",
entityTypes: {
e: {
speed: 1 / 32, // in fractions of a tile per frame
color: "#00F",
size: {x:16, y:24},
pathName: "standard",
},
f: {
speed: 1 / 16, // in fractions of a tile per frame
color: "#08F",
size: {x:18, y:18},
pathName: "complex",
},
},
tileTypes: {
grass: {
style: {baseColor: "#0C3", object: false, sprite: false},
state: {passable: true}
},
wall: {
style: {baseColor: "#D35", object: false, sprite: false},
state: {passable: false}
},
},
}
const testMap = {
displayChars: {
" " : "grass", // what characters mean
"#" : "wall",
"E" : "grass", // also entity spawn
"F" : "grass", // also entity spawn
},
special: { // spawn enties and what not
"E"(idx) { entities.push(new Entity(config.entityTypes.e, idx)) },
"F"(idx) { entities.push(new Entity(config.entityTypes.f, idx)) }
},
map: // I double the width and ignor every second characters as text editors tend to make chars thinner than high
// 0_1_2_3_4_5_6_7_8_9_ x coord
"####################\n" +
"##FF ## ##\n" +
"## ## ##\n" +
"## #### ##\n" +
"## ##\n" +
"## #### ##\n" +
"## ##\n" +
"## ##\n" +
"## EE##\n" +
"####################",
// 0_1_2_3_4_5_6_7_8_9_ x coord
}
const entities = Object.assign([],{
update() {
for (const entity of entities) { entity.update() }
},
draw(ctx) {
for (const entity of entities) { entity.draw(ctx) }
},
});
const tileMap = {
map: [],
mapToIndex(x, y) { return x + y * tileMap.width },
pxToIndex(x, y) { return x / config.tile.size | 0 + (y / config.tile.size | 0) * tileMap.width },
tileByIdx(idx) { return tileMap.map[idx] },
tileByIdxDir(idx, dir) { return tileMap.map[idx + dir.x + dir.y * tileMap.width] },
idxByDir(dir) { return dir.x + dir.y * tileMap.width },
create(mapConfig) {
tileMap.length = 0;
const rows = mapConfig.map.split("\n");
tileMap.width = rows[0].length / 2 | 0;
tileMap.height = rows.length;
canvas.width = tileMap.width * config.tile.size;
canvas.height = tileMap.height * config.tile.size;
var x, y = 0;
while (y < tileMap.height) {
const row = rows[y];
for (x = 0; x < tileMap.width; x += 1) {
const char = row[x * 2];
tileMap.map.push(new Tile(mapConfig.displayChars[char], x, y));
if (mapConfig.special[char]) {
mapConfig.special[char](tileMap.mapToIndex(x, y));
}
}
y++;
}
},
update () {}, // stub
draw(ctx) {
for (const tile of tileMap.map) { tile.draw(ctx) }
},
};
function Tile(typeName, x, y) {
typeName = config.tileTypes[typeName] ? typeName : config.defaultTileName;
const t = config.tileTypes[typeName];
this.idx = x + y * tileMap.width;
this.coord = point(x * config.tile.size, y * config.tile.size);
this.style = {...t.style};
this.state = {...t.state};
}
Tile.prototype = {
draw(ctx) {
ctx.fillStyle = this.style.baseColor;
ctx.fillRect(this.coord.x, this.coord.y, config.tile.size, config.tile.size);
}
};
function Path(pathName) {
if (typeof config.pathTypes[pathName] === "string") {
config.pathTypes[pathName] = dirs.strToDirIdxs(config.pathTypes[pathName]);
}
this.indexes = config.pathTypes[pathName];
this.current = -1;
}
Path.prototype = {
nextDir(tileIdx) {
var len = this.indexes.length;
while (len--) { // make sure we dont loop forever
const dirIdx = this.indexes[this.current];
if (dirIdx > - 1) {
const canMove = tileMap.tileByIdxDir(tileIdx, dirs[dirIdx]).state.passable;
if (canMove) { return dirs[dirIdx] }
}
this.current = (this.current + 1) % this.indexes.length;
}
}
};
function Entity(type, tileIdx) {
this.coord = point();
this.move = point();
this.color = type.color;
this.speed = type.speed;
this.size = {...type.size};
this.path = new Path(type.pathName);
this.pos = this.nextTileIdx = tileIdx;
this.traveled = 1; // unit dist between tiles 1 forces update to find next direction
}
Entity.prototype = {
set dir(dir) {
if (dir === undefined) { // dont move
this.move.y = this.move.x = 0;
this.nextTileIdx = this.tileIdx;
} else {
this.move.x = dir.x * config.tile.size;
this.move.y = dir.y * config.tile.size;
this.nextTileIdx = this.tileIdx + tileMap.idxByDir(dir);
}
},
set pos(tileIdx) {
this.tileIdx = tileIdx;
const tile = tileMap.map[tileIdx];
this.coord.x = tile.coord.x + config.tile.size / 2;
this.coord.y = tile.coord.y + config.tile.size / 2;
this.traveled = 0;
},
draw(ctx) {
const ox = this.move.x * this.traveled;
const oy = this.move.y * this.traveled;
ctx.fillStyle = this.color;
ctx.fillRect(ox + this.coord.x - this.size.x / 2, oy + this.coord.y - this.size.y / 2, this.size.x, this.size.y)
},
update(){
this.traveled += this.speed;
if (this.traveled >= 1) {
this.pos = this.nextTileIdx;
this.dir = this.path.nextDir(this.tileIdx);
}
}
};
const game = {
currentStateName: undefined,
currentState: undefined,
set state(str) {
if (game.states[str]) {
if (game.currentState && game.currentState.end) { game.currentState.end() }
game.currentStateName = str;
game.currentState = game.states[str];
if (game.currentState.start) { game.currentState.start() }
}
},
states: {
setup: {
start() {
tileMap.create(testMap);
game.state = "play";
},
end() {
requestAnimationFrame(game.render); // start the render loop
delete game.states.setup; // MAKE SURE THIS STATE never happens again
},
},
play: {
render(ctx) {
tileMap.update();
entities.update();
tileMap.draw(ctx);
entities.draw(ctx);
}
}
},
renderTo: ctx,
startTime: undefined,
time: 0,
render(time) {
if (game.startTime === undefined) { game.startTime = time }
game.time = time - game.startTime;
if (game.currentState && game.currentState.render) { game.currentState.render(game.renderTo) }
requestAnimationFrame(game.render);
}
};
body{
background-color: #000;
}
canvas{
image-rendering: pixelated;
position: absolute;
top: 0;
left: 0;
width: 400px;
height: 400px;
}
<canvas id="save" width="200" height="200" style="z-index: 1;"></canvas>
Please Note that there are some running states that have not been tested and as such may have a typo.
Also the tile map must be walled to contain entities or they will throw when they try to leave the playfield.
The code is designed to run in the snippet. To make it work in a standard page add above the very first line setTimeout(() => game.state = "setup", 0); the line addEventListener(load", () = { and after the very last line add the line });
I'm attempting to make a game, and I've got some sprites that will overlap each other. When clearing the sprite's area with clearRect, any sprite behind it will disappear as if the foreground sprite wasn't transparent. However, if I try to save and restore the area behind the sprite using get/putImageData, weird things start to happen. Part of the sprites in various places do not get "undrawn", other parts seem to be broken up and yards away, and other sprites get smeared. Here's a code chunk:
var anim = function()
{
if(gtiming < Date.now() % 1000)
timing = (Date.now() % 1000) - gtiming;
if(stage == 1)
{
ugcnt = ugcnt + timing;
if(mleft == true)
{
acc = acc - 0.25;
}
else if(mright == true)
{
acc = acc + 0.25;
}
else
{
if(acc < 0 || acc > 0) acc = acc / 1.1;
}
if(kyx < 0)
{
kyx = 0;
acc = -acc;
}
else if(kyx > 432)
{
kyx = 432;
acc = -acc;
}
if(kyblk != null)
xbios.putImageData(kyblk, kyx, 155);
kyblk = null;
kyx = kyx + acc;
kyblk = xbios.getImageData(kyx, 155, 208, 245);
xbios.drawImage(kyk[Math.floor(kyf)], 0, 0, 416, 490, kyx, 155, 208, 245);
if(ugcnt > mus[r][1] * 1000)
{
ugcnt = 0;
ugobj.push(new ugnaut(Math.floor(Math.random() * 640), -208, "L"));
}
ugobj.forEach(testug);
kyf = kyf + ((timing / 1000) * (mus[r][1] * 240));
if(kyf > 119)
kyf = kyf - 119;
}
gtiming = Date.now() % 1000;
if(stage > 0)
requestAnimationFrame(anim);
}
function ugnaut(x, y, f)
{
this.x = x;
this.y = y;
this.f = f;
this.fr = 0;
this.blk = null;
this.set=function()
{
if(this.blk != null)
xbios.putImageData(this.blk, this.x, this.y);
this.blk = null;
if(f == "L")
{
this.y++;
this.blk = xbios.getImageData(this.x, this.y, 179, 208);
xbios.drawImage(ugf[this.fr], 0, 0, 179, 208, this.x, this.y, 179, 208);
this.fr++;
if(this.fr > 44) this.fr = 0;
}
}
this.getx = function()
{
return this.x;
}
this.gety = function()
{
return this.y;
}
this.getf = function()
{
return this.f;
}
}
function testug(item, index)
{
if(item.getx() > -180 && item.getx() < 640 && item.gety() > -224 && item.gety() < 400)
{
item.set();
}
else
{
item = null;
ugobj.splice(index, 1);
}
}
For those wondering, yes, I did call the Canvas 2D Context xbios. Just felt like a fun name at the time. Anyways, from my understanding having a this inside the "object" ugnaut the value it holds will be local to that object's instance, and so I assume each ugnaut will hold its own background information in this.blk, but am I wrong? What other methods should I use?
I have a selection menu in my HTML canvas that I would like to trigger corresponding audio files. I have tried implementing this by declaring the images inside the if (this.hovered) & (this.clicked) part of the makeSelection function within the selectionForMenu prototype, such that on each new selection the selected audio file is redefined, but this causes problems like slow loading and overlapping audio. It is also problematic as I am trying to get the speaker button at the bottom of the screen to play the audio corresponding to the current selection too, so if it is only defined within that function it is not accessible to the makeButton function.
You can see the selection menu and speaker button in the snippet below. Each new selection in the menu should play once an audio file that corresponds to it (which I have not been able to add to this demonstration). It can be replayed by re-clicking the selection or clicking the speaker button, but each click should only provoke one play of the audio and of course overlapping is undesired. Any help will be appreciated.
var c=document.getElementById('game'),
canvasX=c.offsetLeft,
canvasY=c.offsetTop,
ctx=c.getContext('2d');
var button = function(id, x, strokeColor) {
this.id = id;
this.x = x;
this.strokeColor = strokeColor;
this.hovered = false;
this.clicked = false;
}
button.prototype.makeInteractiveButton = function() {
if (this.hovered) {
if (this.clicked) {
this.fillColor = '#DFBCDE';
} else {
this.fillColor = '#CA92C8'
}
} else {
this.fillColor = '#BC77BA'
}
ctx.strokeStyle=this.strokeColor;
ctx.fillStyle=this.fillColor;
ctx.beginPath();
ctx.lineWidth='5';
ctx.arc(this.x, 475, 20, 0, 2*Math.PI);
ctx.closePath();
ctx.stroke();
ctx.fill();
}
button.prototype.hitTest = function(x, y) {
return (Math.pow(x-this.x, 2) + Math.pow(y-475, 2) < Math.pow(20, 2));
}
var selectionForMenu = function(id, text, y) {
this.id = id;
this.text = text;
this.y = y;
this.hovered = false;
this.clicked = false;
this.lastClicked = false;
}
selectionForMenu.prototype.makeSelection = function() {
var fillColor='#A84FA5';
if (this.hovered) {
if (this.clicked) {
if (this.lastClicked) {
fillColor='#E4C7E2';
} else {
fillColor='#D5A9D3';
}
} else if (this.lastClicked) {
fillColor='#D3A4D0';
} else {
fillColor='#BA74B7';
}
} else if (this.lastClicked) {
fillColor='#C78DC5';
} else {
fillColor='#A84FA5';
}
ctx.beginPath();
ctx.fillStyle=fillColor;
ctx.fillRect(0, this.y, 350, 30)
ctx.stroke();
ctx.font='10px Noto Sans';
ctx.fillStyle='white';
ctx.textAlign='left';
ctx.fillText(this.text, 10, this.y+19);
}
selectionForMenu.prototype.hitTest = function(x, y) {
return (x >= 0) && (x <= (350)) && (y >= this.y) && (y <= (this.y+30)) && !((x >= 0) && (y > 450));
}
var Paint = function(element) {
this.element = element;
this.shapes = [];
}
Paint.prototype.addShape = function(shape) {
this.shapes.push(shape);
}
Paint.prototype.render = function() {
ctx.clearRect(0, 0, this.element.width, this.element.height);
for (var i=0; i<this.shapes.length; i++) {
try {
this.shapes[i].makeSelection();
}
catch(err) {}
}
ctx.beginPath();
ctx.fillStyle='#BC77BA';
ctx.fillRect(0, 450, 750, 50);
ctx.stroke();
for (var i=0; i<this.shapes.length; i++) {
try {
this.shapes[i].makeInteractiveButton();
}
catch(err) {}
}
var speaker = new Image(25, 25);
speaker.src='https://i.stack.imgur.com/lXg2I.png';
ctx.drawImage(speaker, 162.5, 462.5);
}
Paint.prototype.setHovered = function(shape) {
for (var i=0; i<this.shapes.length; i++) {
this.shapes[i].hovered = this.shapes[i] == shape;
}
this.render();
}
Paint.prototype.setClicked = function(shape) {
for (var i=0; i<this.shapes.length; i++) {
this.shapes[i].clicked = this.shapes[i] == shape;
}
this.render();
}
Paint.prototype.setUnclicked = function(shape) {
for (var i=0; i<this.shapes.length; i++) {
this.shapes[i].clicked = false;
if (Number.isInteger(this.shapes[i].id)) {
this.shapes[i].lastClicked = this.shapes[i] == shape;
}
}
this.render();
}
Paint.prototype.select = function(x, y) {
for (var i=this.shapes.length-1; i >= 0; i--) {
if (this.shapes[i].hitTest(x, y)) {
return this.shapes[i];
}
}
return null
}
var paint = new Paint(c);
var btn = new button('speaker', 175, '#FFFCF8');
var selection = [];
for (i=0; i<15; i++) {
selection.push(new selectionForMenu(i+1, i, i*30));
}
paint.addShape(btn);
for (i=0; i<15; i++) {
paint.addShape(selection[i])
}
paint.render();
function mouseDown(event) {
var x = event.x - canvasX;
var y = event.y - canvasY;
var shape = paint.select(x, y);
paint.setClicked(shape);
}
function mouseUp(event) {
var x = event.x - canvasX;
var y = event.y - canvasY;
var shape = paint.select(x, y);
paint.setUnclicked(shape);
}
function mouseMove(event) {
var x = event.x - canvasX;
var y = event.y - canvasY;
var shape = paint.select(x, y);
paint.setHovered(shape);
}
c.addEventListener('mousedown', mouseDown);
c.addEventListener('mouseup', mouseUp);
c.addEventListener('mousemove', mouseMove);
canvas {
z-index: -1;
margin: 1em auto;
border: 1px solid black;
display: block;
background: #9F3A9B;
}
img {
z-index: 0;
position: absolute;
pointer-events: none;
}
#speaker {
top: 480px;
left: 592px;
}
#snail {
top: 475px;
left: 637.5px;
}
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>uTalk Demo</title>
<link rel='stylesheet' type='text/css' href='wordpractice.css' media='screen'></style>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
</head>
<body>
<canvas id="game" width = "350" height = "500"></canvas>
<script type='text/javascript' src='wordpractice copy.js'></script>
</body>
</html>
When you want responsiveness with audio, forget about MediaElements, and go with the Web Audio API.
MediaElements (<audio> and <video>) are slow, and http caching is an nightmare.
With the Web Audio API, you can first download all you media as arrayBuffers, decode their audio data to AudioBuffers, that you'll attach to your js objects.
From there, you'll be able to play new instances of these media in µs.
Beware, ES6 syntax below, for older browsers, here is an ES5 rewrite, also note that Internet Explorer < Edge does not support the Web Audio API, if you need to support these browsers, you'll have to make an fallback with audio elements.
(function myFirstDrumKit() {
const db_url = 'https://dl.dropboxusercontent.com/s/'; // all our medias are stored on dropbox
// we'll need to first load all the audios
function initAudios() {
const promises = drum.parts.map(part => {
return fetch(db_url + part.audio_src) // fetch the file
.then(resp => resp.arrayBuffer()) // as an arrayBuffer
.then(buf => drum.a_ctx.decodeAudioData(buf)) // then decode its audio data
.then(AudioBuf => {
part.buf = AudioBuf; // store the audioBuffer (won't change)
return Promise.resolve(part); // done
});
});
return Promise.all(promises); // when all are loaded
}
function initImages() {
// in this version we have only an static image,
// but we could have multiple per parts, with the same logic as for audios
var img = new Image();
img.src = db_url + drum.bg_src;
drum.bg = img;
return new Promise((res, rej) => {
img.onload = res;
img.onerror = rej;
});
}
let general_solo = false;
let part_solo = false;
const drum = {
a_ctx: new AudioContext(),
generate_sound: (part) => {
// called each time we need to play a source
const source = drum.a_ctx.createBufferSource();
source.buffer = part.buf;
source.connect(drum.gain);
// to keep only one playing at a time
// simply store this sourceNode, and stop the previous one
if(general_solo){
// stop all playing sources
drum.parts.forEach(p => (p.source && p.source.stop(0)));
}
else if (part_solo && part.source) {
// stop only the one of this part
part.source.stop(0);
}
// store the source
part.source = source;
source.start(0);
},
parts: [{
name: 'hihat',
x: 90,
y: 116,
w: 160,
h: 70,
audio_src: 'kbgd2jm7ezk3u3x/hihat.mp3'
},
{
name: 'snare',
x: 79,
y: 192,
w: 113,
h: 58,
audio_src: 'h2j6vm17r07jf03/snare.mp3'
},
{
name: 'kick',
x: 80,
y: 250,
w: 200,
h: 230,
audio_src: '1cdwpm3gca9mlo0/kick.mp3'
},
{
name: 'tom',
x: 290,
y: 210,
w: 110,
h: 80,
audio_src: 'h8pvqqol3ovyle8/tom.mp3'
}
],
bg_src: '0jkaeoxls18n3y5/_drumkit.jpg?dl=0',
};
drum.gain = drum.a_ctx.createGain();
drum.gain.gain.value = .5;
drum.gain.connect(drum.a_ctx.destination);
function initCanvas() {
const c = drum.canvas = document.createElement('canvas');
const ctx = drum.ctx = c.getContext('2d');
c.width = drum.bg.width;
c.height = drum.bg.height;
ctx.drawImage(drum.bg, 0, 0);
document.body.appendChild(c);
addEvents(c);
}
const isHover = (x, y) =>
(drum.parts.filter(p => (p.x < x && p.x + p.w > x && p.y < y && p.y + p.h > y))[0] || false);
function addEvents(canvas) {
let mouse_hovered = false;
canvas.addEventListener('mousemove', e => {
mouse_hovered = isHover(e.pageX - canvas.offsetLeft, e.pageY - canvas.offsetTop)
if (mouse_hovered) {
canvas.style.cursor = 'pointer';
} else {
canvas.style.cursor = 'default';
}
})
canvas.addEventListener('mousedown', e => {
e.preventDefault();
if (mouse_hovered) {
drum.generate_sound(mouse_hovered);
}
});
const checkboxes = document.querySelectorAll('input');
checkboxes[0].onchange = function() {
general_solo = this.checked;
general_solo && (checkboxes[1].checked = part_solo = true);
};
checkboxes[1].onchange = function() {
part_solo = this.checked;
!part_solo && (checkboxes[0].checked = general_solo = false);
};
}
Promise.all([initAudios(), initImages()])
.then(initCanvas);
})()
/*
Audio Samples are from https://sampleswap.org/filebrowser-new.php?d=DRUMS+%28FULL+KITS%29%2FSpasm+Kit%2F
Original image is from http://truimg.toysrus.co.uk/product/images/UK/0023095_CF0001.jpg?resize=500:500
*/
<label>general solo<input type="checkbox"></label><br>
<label>part solo<input type="checkbox"></label><br>
You could create an Audio Loader, that loads all the audios and keeps track of them:
function load(srcs){
var obj={};
srcs.forEach(src=>obj[src]=new Audio(src));
return obj;
}
Then you could do sth like this onload:
var audios=load(["audio1.mp3", "audio2.mp3"]);
And later:
(audios[src] || new Audio(src)).play();
This will just load the audio if it isnt already in the audios object.
Using something like:
window.addEventListener("keydown", handleFn, true);
How would I be able to handle multiple keypresses at the same time, for multiplayer use? Multiple people would be using one keyboard, so like the Q and P keys would be pressed at the same time to move different objects on screen.
I don't have any keyup handles yet and wonder if that would solve this.
The logic I have so far is something like:
if keydown == Q
paddle.left = true;
...
//game loop
if paddle.left == true
paddle.x -= 1;
paddle.left = false;
Players can be expected to hold the button(s) as well.
This is how I generally do it. First you need an array to hold the keystates.
var keys=[];
Then setup your event listeners.
// key events
document.body.addEventListener("keydown", function (e) {
keys[e.keyCode] = true;
});
document.body.addEventListener("keyup", function (e) {
keys[e.keyCode] = false;
});
What the following does is set an item in the array to either true or false, corresponding to that keys code.
Then you just need to use some conditions to see what is pressed and what you should do.
// check the keys and do the movement.
if (keys[38]) {
if (velY > -speed) {
velY--;
}
}
if (keys[40]) {
if (velY < speed) {
velY++;
}
}
if (keys[39]) {
if (velX < speed) {
velX++;
}
}
if (keys[37]) {
if (velX > -speed) {
velX--;
}
}
Below is a demo where you can move around and mess with multiple key presses. Use wasd, and the arrow keys.
Live Demo
var canvas = document.getElementById("canvas"),
ctx = canvas.getContext("2d");
canvas.width = canvas.height = 300;
var player1 = {
x: 50,
y: 150,
velY: 0,
velX: 0,
color: "blue"
},
player2 = {
x: 250,
y: 150,
velY: 0,
velX: 0,
color: "red"
};
var x = 150,
y = 150,
velY = 0,
velX = 0,
speed = 2,
friction = 0.98,
keys = [];
function update() {
if (keys[38]) {
if (player1.velY > -speed) {
player1.velY--;
}
}
if (keys[40]) {
if (player1.velY < speed) {
player1.velY++;
}
}
if (keys[39]) {
if (player1.velX < speed) {
player1.velX++;
}
}
if (keys[37]) {
if (player1.velX > -speed) {
player1.velX--;
}
}
if (keys[87]) {
if (player2.velY > -speed) {
player2.velY--;
}
}
if (keys[83]) {
if (player2.velY < speed) {
player2.velY++;
}
}
if (keys[68]) {
if (player2.velX < speed) {
player2.velX++;
}
}
if (keys[65]) {
if (player2.velX > -speed) {
player2.velX--;
}
}
ctx.clearRect(0, 0, 300, 300);
updatePlayer(player1);
updatePlayer(player2);
setTimeout(update, 10);
}
function updatePlayer(player) {
player.velY *= friction;
player.y += player.velY;
player.velX *= friction;
player.x += player.velX;
if (player.x >= 295) {
player.x = 295;
} else if (player.x <= 5) {
player.x = 5;
}
if (player.y > 295) {
player.y = 295;
} else if (player.y <= 5) {
player.y = 5;
}
ctx.fillStyle = player.color;
ctx.beginPath();
ctx.arc(player.x, player.y, 5, 0, Math.PI * 2);
ctx.fill();
}
update();
document.body.addEventListener("keydown", function (e) {
keys[e.keyCode] = true;
});
document.body.addEventListener("keyup", function (e) {
keys[e.keyCode] = false;
});
You could try a pattern like this:
(function game(){
// canvas setup ...
// set up a "hash" of keycodes associated with whether or not they
// are pressed, and what should happen when they are pressed.
var keys = {
37:{down:false, action:function(){player1.velX--;}},
38:{down:false, action:function(){player1.velY--;}},
39:{down:false, action:function(){player1.velX++;}},
40:{down:false, action:function(){player1.velY++;}},
65:{down:false, action:function(){player2.velX--;}},
68:{down:false, action:function(){player2.velX++;}},
83:{down:false, action:function(){player2.velY++;}},
87:{down:false, action:function(){player2.velY--;}},
};
document.body.addEventListener("keydown", function (e) {
if(keys[e.keyCode]) keys[e.keyCode].down = true;
});
document.body.addEventListener("keyup", function (e) {
if(keys[e.keyCode]) keys[e.keyCode].down = false;
});
(function update() {
ctx.clearRect(...);
for(var key in keys)
if(keys[key].down)
keys[key].action();
// redraw players.
requestAnimationFrame(update);
})();
})();
The nice thing about this setup is that it associates the actions directly with the keys, allows you to add more key-actions easily, and allows for a great amount of flexibility with the possibility of easily adding/removing key presses at runtime, and even changing what a particular key does at any given time.